Implementation Patterns

Pages & Dynamic Routes

Brease sites use a single optional catch-all route ([[...slug]]) to handle every page, including the homepage. This page explains the route structure, layout, page component, static generation, and section components in detail.


Route Structure

app/
  [[...slug]]/
    layout.tsx    ← BreaseContext wrapper (server component)
    page.tsx      ← Page rendering + metadata (server component)

The double-bracket [[...slug]] syntax makes the slug parameter optional. This means:

  • / matches with slug as undefined
  • /about matches with slug as ['about']
  • /sk/about-us matches with slug as ['sk', 'about-us']

No separate locale segment needed

You do not need a [locale] route segment. Locale prefixes (e.g. sk/about-us) are part of the slug and handled automatically by fetchPage and BreaseContext.


Layout (layout.tsx)

The layout is a server component that wraps every page with BreaseContext. It receives the slug from route params and passes it to the context provider.

// src/app/[[...slug]]/layout.tsx
import { BreaseContext } from 'brease-next'
import { contextData } from '@/lib/brease/config'
import { getPage } from '@/lib/brease/get-page'

export default async function CatchAllLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const slugStr = (slug ?? []).join('/')

  return (
    <BreaseContext config={contextData} slug={slugStr} getPage={getPage}>
      {children}
    </BreaseContext>
  )
}

What BreaseContext does

When the layout renders, BreaseContext:

  1. Fetches the page data using getPage (or fetchPage internally if getPage is not provided)
  2. Fetches all configured navigations and collections (with the correct locale derived from the slug)
  3. Fetches available locales and alternate links
  4. Makes all of this data available to child client components via the useBrease() hook

The getPage function

Using React cache ensures the page is only fetched once per render pass, even if both the layout and page component call it:

// src/lib/brease/get-page.ts
import { cache } from 'react'
import { fetchPage } from 'brease-next/server'

export const getPage = cache(async (slug: string) => {
  return fetchPage(slug)
})

Page (page.tsx)

The page component handles three concerns: static generation, metadata, and rendering.

// src/app/[[...slug]]/page.tsx
import { notFound } from 'next/navigation'
import {
  BreasePage,
  BreaseStructuredData,
  BreaseCustomCode,
  generateBreasePageParams,
  generateBreasePageMetadata,
  ensureSuccess,
} from 'brease-next'
import { componentMap } from '@/lib/brease/config'
import { getPage } from '@/lib/brease/get-page'

// 1. Static generation — tell Next.js which pages to pre-render
export async function generateStaticParams() {
  return generateBreasePageParams()
  // Returns: [
  //   { locale: 'en', slug: ['about'] },
  //   { locale: 'sk', slug: ['sk', 'o-nas'] },
  //   ...
  // ]
}

// 2. Metadata — SEO tags, Open Graph, Twitter Cards
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const slugStr = (slug ?? []).join('/')
  const result = await getPage(slugStr)

  if (!result.success) return {}

  return generateBreasePageMetadata(result.data, {
    metadataBase: 'https://example.com',
  })
}

// 3. Rendering — fetch page and render sections
export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const slugStr = (slug ?? []).join('/')
  const result = await getPage(slugStr)

  if (!result.success) {
    notFound()
  }

  const page = ensureSuccess(result)

  return (
    <>
      <BreaseStructuredData page={page} />
      <BreasePage page={page} sectionMap={componentMap} />
      <BreaseCustomCode page={page} />
    </>
  )
}

generateStaticParams

generateBreasePageParams() iterates all locales and all pages, returning an array of { locale: string; slug: string[] } objects. Next.js uses this to pre-render every page at build time.

generateMetadata

generateBreasePageMetadata takes a page object (not a slug string) and returns a Next.js Metadata object containing:

  • title and description from the page's meta fields
  • openGraph data (title, description, image, type)
  • twitterCard data
  • robots indexing directives based on page.indexing
  • canonicalUrl if set
  • alternates for multi-locale sites

The optional metadataBase is prepended to relative URLs in Open Graph images and canonical URLs.

404 Handling

If getPage returns a failed response (page not found or API error), call notFound() from next/navigation. This triggers Next.js to render your not-found.tsx page with a 404 status code.

// src/app/not-found.tsx
export default function NotFound() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <h1 className="text-4xl font-bold">404 — Page Not Found</h1>
    </div>
  )
}

Section Components

BreasePage iterates through page.sections and renders a component for each one by matching section.key against the keys in your sectionMap.

Matching uses section.key

Sections are matched by their key field, not by section.name or any other property. Make sure the keys in your componentMap exactly match the keys assigned to sections in the CMS.

Section component rules

  1. Section components must be client components — add 'use client' at the top of every section file. BreasePage renders sections inside a client component tree, so they cannot be server components.
  2. They receive the section's elements spread as props
  3. Type the props with a custom interface that matches your CMS schema

Example: HeroSection

Suppose your CMS has a section with key hero and elements: title (text), subtitle (text), image (media).

// src/sections/hero-section.tsx
'use client'

import { BreaseImage, type BreaseMedia } from 'brease-next'

interface HeroSectionProps {
  title: string
  subtitle: string
  image: BreaseMedia
}

export default function HeroSection({
  title,
  subtitle,
  image,
}: HeroSectionProps) {
  return (
    <section className="relative py-24 px-6 text-center">
      <h1 className="text-5xl font-bold">{title}</h1>
      {subtitle && <p className="mt-4 text-xl text-gray-600">{subtitle}</p>}
      {image && (
        <div className="mt-12 mx-auto max-w-4xl">
          <BreaseImage breaseImage={image} variant="xl" priority />
        </div>
      )}
    </section>
  )
}

Registering sections

// src/lib/brease/config.ts
import { ComponentType } from 'react'
import { SectionElementProps } from 'brease-next'
import HeroSection from '@/sections/hero-section'
import TextSection from '@/sections/text-section'
import CtaSection from '@/sections/cta-section'

export const componentMap: Record<string, ComponentType<SectionElementProps>> = {
  hero: HeroSection as unknown as ComponentType<SectionElementProps>,
  text: TextSection as unknown as ComponentType<SectionElementProps>,
  cta: CtaSection as unknown as ComponentType<SectionElementProps>,
}

Handling rich text elements

Rich text fields from the CMS contain HTML. Render them with dangerouslySetInnerHTML:

'use client'

interface TextSectionProps {
  heading: string
  body: string  // HTML from the CMS rich text editor
}

export default function TextSection({ heading, body }: TextSectionProps) {
  return (
    <section className="py-16 px-6 max-w-3xl mx-auto">
      <h2 className="text-3xl font-bold mb-6">{heading}</h2>
      <div
        className="prose prose-lg"
        dangerouslySetInnerHTML={{ __html: body }}
      />
    </section>
  )
}

Missing section keys

If a section's key has no matching entry in componentMap, a warning is logged to the console and the section is skipped. Other sections continue rendering normally.


Next Steps

Previous
Project Setup Guide