Documentation

Implementation Guide

Step-by-step guide to implementing Brease CMS in your Next.js application using the brease-next package.

Prerequisites

Before starting, ensure you have:


Step 1: Set Up Root Layout

The root layout wraps your app with BreaseContext and provides global data.

Create src/app/layout.tsx

import type { Metadata } from 'next';
import { BreaseContext, fetchSite } from 'brease-next';
import { contextData } from '@/lib/brease-config';

import './globals.css';

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}`,
    };
  }

  return {
    title: 'My Site',
    description: 'Site description',
  };
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <BreaseContext config={contextData}>
          {children}
        </BreaseContext>
      </body>
    </html>
  );
}

What This Does

  • Fetches site data to populate metadata
  • Creates a template for page titles (Page Title | Site Name)
  • Wraps application with BreaseContext using your configuration
  • Provides consistent HTML structure

Step 2: Create Homepage

The homepage is typically at the root slug (/).

Create 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 Points

  • dynamic = 'force-static' ensures the page is statically generated
  • generateMetadata() creates SEO tags from Brease data
  • Error handling returns 404 or throws error for error boundary
  • BreasePage renders all sections dynamically

Step 3: Create Dynamic Pages Route

Handle all other pages with a dynamic route.

Create 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 Static Generation Works

  1. At build time, generateStaticParams() fetches all pages from Brease
  2. Next.js pre-renders each page
  3. At runtime, if a page doesn't exist, returns 404
  4. Use dynamic = 'auto' to allow ISR (Incremental Static Regeneration)

Step 4: Create Section Components

Section components receive data from Brease sections and render the content.

Example: Hero Section

// 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">
        <div className="mx-auto max-w-7xl px-6 py-24">
          <div className="mx-auto max-w-2xl">
            <h1 className="text-5xl font-semibold">
              {title}
            </h1>
            <div
              className="mt-8 text-lg"
              dangerouslySetInnerHTML={{ __html: body }}
            />
          </div>
          <div className="mt-16">
            <BreaseImage breaseImage={heroMedia} />
          </div>
        </div>
      </div>
    </div>
  );
}

Section Prop Types

Define props based on your Brease section schema:

interface TextSectionProps {
  heading: string;
  content: string;
  alignment?: string;
}

interface ImageGridSectionProps {
  images: BreaseMedia[];
  columns: number;
}

interface CtaSectionProps {
  title: string;
  buttonText: string;
  buttonLink: string;
  backgroundColor?: string;
}

Handling Rich Text

Use dangerouslySetInnerHTML for HTML content from Brease:

<div dangerouslySetInnerHTML={{ __html: body }} />

Note: Ensure content is sanitized in Brease or add a sanitization library.


Step 5: Map Sections and Configure Context

Connect Brease section types to your React components and configure global data.

Update 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 ImageGridSection from '@/sections/image-grid-section';
import CtaSection from '@/sections/cta-section';

type SectionProps = Record<string, unknown>;

// Section mapping configuration
export const sectionMap: Record<string, ComponentType<SectionProps>> = {
  hero: HeroSection,
  smallHero: SmallHeroSection,
  text: TextContentSection,
  imageGrid: ImageGridSection,
  cta: CtaSection,
};

// 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' },
  ],
};

Naming Convention

  • Brease section type: hero (lowercase, camelCase)
  • Component name: HeroSection (PascalCase)
  • Map key: hero (matches Brease exactly)

Step 6: Create Navigation Component

Use the useBrease hook to access navigation data.

Create src/components/navigation.tsx

'use client';

import { useBrease } from 'brease-next';
import Link from 'next/link';

export default function Navigation() {
  const { navigations } = useBrease();
  const { mainNavigation } = navigations;

  if (!mainNavigation) {
    return null;
  }

  return (
    <nav className="bg-white shadow">
      <div className="mx-auto max-w-7xl px-6">
        <div className="flex h-16 items-center justify-between">
          <div className="flex space-x-8">
            {mainNavigation.items.map((item, index) => {
              if (item.type === 'internal' && item.target) {
                return (
                  <Link
                    key={index}
                    href={item.target.slug}
                    className="text-gray-900 hover:text-gray-600"
                  >
                    {item.value}
                  </Link>
                );
              }

              if (item.url) {
                return (
                  <a
                    key={index}
                    href={item.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-gray-900 hover:text-gray-600"
                  >
                    {item.value}
                  </a>
                );
              }

              return null;
            })}
          </div>
        </div>
      </div>
    </nav>
  );
}
  • Internal Link: Has item.target.slug (links to Brease pages)
  • External Link: Has item.url (links to external URLs)

Step 7: Implement Collection Routes

For blog posts, products, or other collection-based content.

Create src/app/news/[slug]/page.tsx

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { fetchEntryBySlug, generateBreaseCollectionParams, type BreaseMedia } from 'brease-next';

export const dynamic = 'auto';

const NEWS_COLLECTION_ID = 'col-a01c8223-4e4a-40aa-90d9-70149e87322c';

export async function generateStaticParams() {
  return await generateBreaseCollectionParams(NEWS_COLLECTION_ID);
}

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) {
    return {};
  }

  const entry = result.data;

  return {
    title: entry.elements.title as string,
    description: entry.elements.excerpt as string,
    openGraph: {
      title: entry.elements.title as string,
      description: entry.elements.excerpt as string,
      type: 'article',
      images: entry.elements.featuredImage
        ? [{ url: (entry.elements.featuredImage as BreaseMedia).path }]
        : undefined,
    },
  };
}

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 article: ${result.error}`);
  }

  const entry = result.data;
  const title = entry.elements.title as string;
  const content = entry.elements.content as string;
  const featuredImage = entry.elements.featuredImage as BreaseMedia;

  return (
    <article className="mx-auto max-w-4xl px-6 py-12">
      <h1 className="text-4xl font-bold mb-4">{title}</h1>
      {featuredImage && (
        <img
          src={featuredImage.path}
          alt={featuredImage.alt}
          className="w-full rounded-lg mb-8"
        />
      )}
      <div
        className="prose"
        dangerouslySetInnerHTML={{ __html: content }}
      />
    </article>
  );
}

Finding Collection IDs

Collection IDs are UUIDs from Brease. To find them:

  1. Open Brease dashboard
  2. Navigate to Collections
  3. Copy the UUID from the URL or collection settings
  4. Add it to contextData in brease-config.ts

Step 8: Use Collections from Context

Access collection data from the context in client components.

Example: Latest News Section

'use client';

import { useBrease } from 'brease-next';
import Link from 'next/link';

export default function LatestNewsSection() {
  const { collections } = useBrease();
  const { news } = collections;

  if (!news || news.length === 0) {
    return null;
  }

  return (
    <div className="mx-auto max-w-7xl px-6 py-12">
      <h2 className="text-3xl font-bold mb-8">Latest News</h2>
      <div className="grid gap-8 md:grid-cols-3">
        {news.slice(0, 3).map((article) => {
          const title = article.elements.title as string;
          const excerpt = article.elements.excerpt as string;

          return (
            <Link
              key={article.slug}
              href={`/news${article.slug}`}
              className="block group"
            >
              <article className="border rounded-lg overflow-hidden hover:shadow-lg transition">
                <div className="p-6">
                  <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600">
                    {title}
                  </h3>
                  <p className="text-gray-600">{excerpt}</p>
                </div>
              </article>
            </Link>
          );
        })}
      </div>
    </div>
  );
}

Step 9: Build and Test

Development

npm run dev

Visit http://localhost:3000 to test your implementation.

Build for Production

npm run build

This will:

  • Fetch all pages and collections from Brease
  • Generate static HTML for each page
  • Fetch and apply redirects
  • Optimize images

Test Static Build

npm run build
npm start

Common Patterns

Loading States

Handle loading states in client components:

'use client';

import { useBrease } from 'brease-next';

export default function Navigation() {
  const { navigations } = useBrease();
  const { mainNavigation } = navigations;

  if (!mainNavigation) {
    return <div>Loading...</div>;
  }

  return (
    <nav>
      {/* navigation items */}
    </nav>
  );
}

Error Boundaries

Create an error boundary for better error handling:

// src/app/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Not Found Pages

Create a custom 404 page:

// src/app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </div>
  );
}

Deployment Checklist

  • All environment variables set in production
  • Build completes successfully
  • All pages render correctly
  • Navigation works
  • Images load properly
  • Metadata appears in page source
  • Redirects work as expected
  • 404 pages return correct status code

Next Steps

Review Examples for complete working code samples, or consult Troubleshooting if you encounter issues.

Previous
Components