Documentation
Examples
Complete, production-ready examples from the sample project showing real implementations using brease-next.
Configuration File
Central configuration for section mapping and context data.
File: src/lib/brease-config.ts
import { ComponentType } from 'react';
import HeroSection from '@/sections/hero-section';
import SmallHeroSection from '@/sections/small-hero-section';
import TextContentSection from '@/sections/text-content-section';
import LatestNewsSection from '@/sections/latest-news-section';
type SectionProps = Record<string, unknown>;
// Section mapping configuration
export const sectionMap: Record<string, ComponentType<SectionProps>> = {
hero: HeroSection,
smallHero: SmallHeroSection,
text: TextContentSection,
latestNews: LatestNewsSection,
};
// Context configuration for navigations and collections
export const contextData = {
navigations: [
{ key: 'mainNavigation', id: 'nav-a01c4cbb-21f7-46d5-a89c-564307998128' },
],
collections: [
{ key: 'news', id: 'col-a01c8223-4e4a-40aa-90d9-70149e87322c' },
],
};
Root Layout
Root layout with metadata generation and context setup.
File: src/app/layout.tsx
import { ReactNode } from "react";
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import FooterSection from '@/sections/footer-section';
import { BreaseContext, fetchSite } from 'brease-next';
import { contextData } from "@/lib/brease-config";
import './globals.css';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export async function generateMetadata(): Promise<Metadata> {
const result = await fetchSite();
if (result.success) {
return {
title: {
template: `%s | ${result.data.name}`,
default: result.data.name,
},
description: `${result.data.name} - ${result.data.domain}`,
};
}
console.error('Failed to fetch site data for metadata:', result);
return {
title: {
template: '%s | Create Next App',
default: 'Create Next App',
},
description: 'Generated by create next app',
};
}
export default function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<BreaseContext config={contextData}>
{children}
<FooterSection />
</BreaseContext>
</body>
</html>
);
}
Key Features
- Fetches site metadata from Brease
- Wraps app with
BreaseContextusingcontextDataconfiguration - Includes global footer component
- Sets up custom fonts
Homepage Implementation
Basic homepage that fetches and renders Brease content.
File: src/app/page.tsx
import type { Metadata } from 'next';
import { fetchPage, fetchSite, BreasePage, generateBreasePageMetadata } from 'brease-next';
import { sectionMap } from '@/lib/brease-config';
import { notFound } from 'next/navigation';
export const dynamic = 'force-static';
export async function generateMetadata(): Promise<Metadata> {
const result = await fetchSite();
const metadata = await generateBreasePageMetadata('/');
if (result.success) {
metadata.title = `${metadata.title} | ${result.data.name}`;
}
return metadata;
}
export default async function Home() {
const result = await fetchPage('/');
if (!result.success) {
if (result.status === 404) {
return notFound();
}
throw new Error(`Failed to load page: ${result.error}`);
}
return <BreasePage page={result.data} sectionMap={sectionMap} />;
}
Key Features
- Static generation with
dynamic = 'force-static' - Automatic metadata from Brease
- Error handling for 404 and other errors
- Dynamic section rendering
Dynamic Route with Sections
Dynamic route for subpages with static generation.
File: src/app/[subpageSlug]/page.tsx
import type { Metadata } from 'next';
import { fetchPage, generateBreasePageParams, generateBreasePageMetadata, BreasePage } from 'brease-next';
import { sectionMap } from '@/lib/brease-config';
import { notFound } from 'next/navigation';
export const dynamic = 'auto';
export async function generateStaticParams() {
return generateBreasePageParams();
}
export async function generateMetadata({ params }: { params: Promise<{ subpageSlug: string }> }): Promise<Metadata> {
const { subpageSlug } = await params;
return generateBreasePageMetadata(`/${subpageSlug}`);
}
export default async function Subpage({ params }: { params: Promise<{ subpageSlug: string }> }) {
const { subpageSlug } = await params;
const result = await fetchPage(`/${subpageSlug}`);
if (!result.success) {
if (result.status === 404) {
return notFound();
}
throw new Error(`Failed to load page: ${result.error}`);
}
return <BreasePage page={result.data} sectionMap={sectionMap} />;
}
How It Works
generateStaticParams()pre-renders all pages at build timegenerateMetadata()creates SEO tags for each page- Page component fetches and renders the content
- Returns 404 if page not found
Collection Entry Route
News article detail page from a collection.
File: src/app/news/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { JsonPrinter } from '@/components/json-printer';
import { fetchEntryBySlug, generateBreaseCollectionParams, type BreaseMedia } from 'brease-next';
import 'react-json-view-lite/dist/index.css';
export const dynamic = 'auto';
const NEWS_COLLECTION_ID = 'col-a01c8223-4e4a-40aa-90d9-70149e87322c';
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const result = await fetchEntryBySlug(NEWS_COLLECTION_ID, `/${slug}`);
if (!result.success) {
console.error('Failed to fetch entry data for metadata:', result);
return {};
}
const collection = result.data;
const collectionDetails = {
title: collection.elements.title as string || 'News article',
slug: `/${slug}`,
description: collection.elements.excerpt as string || 'News article excerpt.',
image: collection.elements.blogPostMedia as BreaseMedia || null
};
const metadata: Metadata = {
title: collectionDetails.title,
};
if (collectionDetails.description) {
metadata.description = collectionDetails.description;
}
metadata.openGraph = {
title: collectionDetails.title,
description: collectionDetails.description,
type: 'article',
url: collectionDetails.slug,
images: collectionDetails.image
? [{ url: collectionDetails.image.path }]
: undefined,
};
return metadata;
}
export async function generateStaticParams() {
return await generateBreaseCollectionParams(NEWS_COLLECTION_ID);
}
export default async function NewsArticle({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const result = await fetchEntryBySlug(NEWS_COLLECTION_ID, `/${slug}`);
if (!result.success) {
if (result.status === 404) {
return notFound();
}
throw new Error(`Failed to load collection entry: ${result.error}`);
}
return <JsonPrinter json={result.data.elements} />;
}
Type Casting Collection Fields
Collection entry elements are untyped. Cast them based on your schema:
const title = entry.elements.title as string;
const content = entry.elements.content as string;
const featuredImage = entry.elements.featuredImage as BreaseMedia;
const publishedAt = entry.elements.publishedAt as string;
const tags = entry.elements.tags as string[];
Section Components
Hero Section
Full-featured hero section with navigation and responsive image.
File: src/sections/hero-section.tsx
'use client';
import { BreaseImage, type BreaseMedia } from 'brease-next';
import Navigation from '@/components/navigation';
interface HeroSectionProps {
title: string;
body: string;
heroMedia: BreaseMedia;
}
export default function HeroSection({ title, body, heroMedia }: HeroSectionProps) {
return (
<div className="bg-white dark:bg-gray-900">
<Navigation />
<div className="relative isolate pt-14">
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 size-full mask-[radial-gradient(100%_100%_at_top_right,white,transparent)] stroke-gray-200 dark:stroke-white/10"
>
<defs>
<pattern
x="50%"
y={-1}
id="hero-pattern"
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path d="M100 200V.5M.5 .5H200" fill="none" />
</pattern>
</defs>
<rect fill="url(#hero-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-center lg:gap-x-10 lg:px-8 lg:py-40">
<div className="mx-auto max-w-2xl lg:mx-0 lg:flex-auto">
<h1 className="mt-10 text-5xl font-semibold tracking-tight text-pretty text-gray-900 sm:text-7xl dark:text-white">
{title}
</h1>
<div className="mt-8 text-lg font-medium text-pretty text-gray-500 sm:text-xl/8 dark:text-gray-400">
<div dangerouslySetInnerHTML={{ __html: body }} />
</div>
</div>
<div className="mt-16 sm:mt-24 lg:mt-0 lg:shrink-0 lg:grow">
<BreaseImage breaseImage={heroMedia} />
</div>
</div>
</div>
</div>
);
}
Latest News Section
Section that receives collection data as props from Brease.
File: src/sections/latest-news-section.tsx
import Link from 'next/link';
import { BreaseImage, type BreaseMedia } from 'brease-next';
interface LatestNewsSectionProps {
title: string;
posts: {
name: string;
entries: {
name: string;
slug: string;
elements: {
title: string;
body: string;
blogPostMedia: BreaseMedia;
excerpt: string;
};
uuid: string;
}[];
};
}
export default function LatestNewsSection({ title, posts }: LatestNewsSectionProps) {
return (
<div className="bg-white py-24 sm:py-32 dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-4xl font-semibold tracking-tight text-balance text-gray-900 sm:text-5xl dark:text-white">
{title}
</h2>
<p className="mt-2 text-lg/8 text-gray-600 dark:text-gray-400">
Learn how to grow your business with our expert advice.
</p>
</div>
<div className="mx-auto mt-16 grid max-w-2xl auto-rows-fr grid-cols-1 gap-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{posts.entries.map((post) => (
<article
key={post.uuid}
className="relative isolate flex flex-col justify-end overflow-hidden rounded-2xl bg-gray-900 px-8 pt-80 pb-8 sm:pt-48 lg:pt-80 dark:bg-gray-800"
>
<BreaseImage
breaseImage={post.elements.blogPostMedia}
className="absolute inset-0 -z-10 size-full object-cover"
/>
<div className="absolute inset-0 -z-10 bg-linear-to-t from-gray-900 via-gray-900/40 dark:from-black/80 dark:via-black/40" />
<div className="absolute inset-0 -z-10 rounded-2xl inset-ring inset-ring-gray-900/10 dark:inset-ring-white/10" />
<h3 className="mt-3 text-lg/6 font-semibold text-white">
<Link href={`/news${post.slug}`}>
<span className="absolute inset-0" />
{post.elements.title}
</Link>
</h3>
<p className="line-clamp-1">{post.elements.excerpt}</p>
</article>
))}
</div>
</div>
</div>
);
}
Navigation Component
Responsive navigation using useBrease hook with the new destructuring pattern.
File: src/components/navigation.tsx
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { useState } from 'react';
import { useBrease } from 'brease-next';
import { Dialog, DialogPanel } from '@headlessui/react';
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
export default function Navigation() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { navigations } = useBrease();
const { mainNavigation } = navigations;
return (
<header className="absolute inset-x-0 top-0 z-50">
<nav aria-label="Global" className="flex items-center justify-between p-6 lg:px-8">
<div className="flex lg:flex-1">
<Link href="/" className="-m-1.5 p-1.5">
<span className="sr-only">Your Company</span>
<Image
alt=""
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
width={1040}
height={626}
className="h-8 w-auto dark:hidden"
/>
<Image
alt=""
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
width={1040}
height={626}
className="h-8 w-auto not-dark:hidden"
/>
</Link>
</div>
<div className="flex lg:hidden">
<button
type="button"
onClick={() => setMobileMenuOpen(true)}
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-gray-200"
>
<span className="sr-only">Open main menu</span>
<Bars3Icon aria-hidden="true" className="size-6" />
</button>
</div>
<div className="hidden lg:flex lg:gap-x-12">
{mainNavigation?.items.map((item) => (
<Link
key={item.value}
href={item?.target?.slug || item?.url || '#'}
className="text-sm/6 font-semibold text-gray-900 dark:text-white"
>
{item.value}
</Link>
))}
</div>
</nav>
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen} className="lg:hidden">
<div className="fixed inset-0 z-50" />
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-gray-900 dark:sm:ring-gray-100/10">
<div className="flex items-center justify-between">
<Link href="/" className="-m-1.5 p-1.5">
<span className="sr-only">Your Company</span>
<Image
alt=""
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
width={1040}
height={626}
className="h-8 w-auto dark:hidden"
/>
<Image
alt=""
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
width={1040}
height={626}
className="h-8 w-auto not-dark:hidden"
/>
</Link>
<button
type="button"
onClick={() => setMobileMenuOpen(false)}
className="-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-200"
>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="size-6" />
</button>
</div>
<div className="mt-6 flow-root">
<div className="-my-6 divide-y divide-gray-500/10 dark:divide-white/10">
<div className="space-y-2 py-6">
{mainNavigation?.items.map((item) => (
<Link
key={item.value}
href={item?.target?.slug || item?.url || '#'}
className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-white/5"
>
{item.value}
</Link>
))}
</div>
</div>
</div>
</DialogPanel>
</Dialog>
</header>
);
}
Key Features
- Uses new
useBrease()destructuring pattern - Accesses
mainNavigationfromnavigationsobject - Responsive mobile menu with Headless UI
- Handles both internal and external links
- Dark mode support
Next.js Configuration
Complete configuration with redirects, images, and CORS.
File: next.config.ts
import type { NextConfig } from "next";
import { fetchRedirects, type BreaseRedirect } from "brease-next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.eu-central-1.amazonaws.com',
pathname: '/**',
}
]
},
async redirects() {
const result = await fetchRedirects();
if (!result.success) {
console.error('Failed to fetch redirects:', result.error);
return [];
}
return result.data.map((redirect: BreaseRedirect) => {
return {
source: redirect.source,
destination: redirect.destination,
permanent: (redirect.permanent === '301' || redirect.permanent === '308'),
}
})
},
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: 'https://app.brease.io, https://dev.brease.io, https://*.vercel.app'
},
],
},
];
},
};
export default nextConfig;
Configuration Features
- Remote Images: S3 bucket pattern for Brease media
- Dynamic Redirects: Fetched from Brease at build time
- CORS Headers: Enables Brease editor preview functionality
- All imports from
brease-nextpackage
Next Steps
For troubleshooting and common issues, see Troubleshooting.