> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Build content-driven Next.js ButterCMS apps. Covers setup, pages, collections, components, and blog content, with Router examples.

## Overview

This integration guide shows you how to how to update your existing project to:

1. install the ButterCMS package
2. instantiate ButterCMS
3. create components to fetch and display each of the three ButterCMS content types: [Pages](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

### Starter project

Or, you can jump directly to the starter project below, which will allow you to clone, install, run, and deploy a fully working
starter project that's integrated with content already inside of your ButterCMS account.

<Card title="Next.js Starter Project" icon="rocket" href="../starter-projects/nextjs">
  Hit the ground running with a pre-configured Next.js + ButterCMS setup.
</Card>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install buttercms
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add buttercms
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add buttercms
    ```
  </Tab>
</Tabs>

Add your API token to `.env.local`:

```bash theme={null}
NEXT_PUBLIC_BUTTER_CMS_API_KEY=your_api_token
```

<Info>
  The **App Router** examples target **Next.js 15+**, where route `params` and
  `draftMode()` are async. On Next.js 14 and earlier, use the synchronous forms
  (`params: { slug: string }`, and `draftMode()` without `await`).
</Info>

## Initialize the client

Create a reusable client instance:

```typescript theme={null}
// lib/buttercms.ts
import Butter from 'buttercms';

const butter = Butter(process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY!);

export default butter;
```

<Info>
  For complete SDK documentation including all available methods and configuration options, see the [JavaScript SDK Reference](../sdks/javascript-sdk).
</Info>

## Pages

<Tabs>
  <Tab title="App Router">
    ```typescript theme={null}
    // app/[slug]/page.tsx
    import butter from '@/lib/buttercms';
    import { notFound } from 'next/navigation';

    interface PageProps {
      params: Promise<{ slug: string }>;
    }

    // Describe your Page's fields so `fields` isn't `object`
    interface LandingPageFields {
      headline: string;
      subheadline: string;
      hero_image?: string;
      body: string;
    }

    export async function generateStaticParams() {
      const response = await butter.page.list<LandingPageFields>('landing_page');
      return (response.data?.data ?? []).map((page) => ({
        slug: page.slug,
      }));
    }

    async function getPage(slug: string) {
      try {
        const response = await butter.page.retrieve<LandingPageFields>('landing_page', slug);
        return response.data?.data ?? null;   // data is optional in the SDK
      } catch {
        return null;
      }
    }

    export default async function LandingPage({ params }: PageProps) {
      const { slug } = await params;
      const page = await getPage(slug);

      if (!page) {
        notFound();
      }

      return (
        <main>
          <h1>{page.fields.headline}</h1>
          <p>{page.fields.subheadline}</p>
          {page.fields.hero_image && (
            <img src={page.fields.hero_image} alt={page.fields.headline} />
          )}
          <div dangerouslySetInnerHTML={{ __html: page.fields.body }} />
        </main>
      );
    }

    // ISR: revalidate every 60 seconds
    export const revalidate = 60;
    ```
  </Tab>

  <Tab title="Pages Router">
    ```typescript theme={null}
    // pages/[slug].tsx
    import butter from '@/lib/buttercms';
    import { GetStaticProps, GetStaticPaths } from 'next';

    interface LandingPageFields {
      headline: string;
      subheadline: string;
      hero_image: string;
      body: string;
    }

    interface PageProps {
      page: {
        fields: LandingPageFields;
      };
    }

    export const getStaticPaths: GetStaticPaths = async () => {
      const response = await butter.page.list<LandingPageFields>('landing_page');
      if (!response.data?.data) {
        throw new Error('Failed to load pages from ButterCMS');
      }
      const paths = response.data.data.map((page) => ({
        params: { slug: page.slug },
      }));
      return { paths, fallback: 'blocking' };
    };

    export const getStaticProps: GetStaticProps<PageProps> = async ({ params }) => {
      try {
        const response = await butter.page.retrieve<LandingPageFields>('landing_page', params?.slug as string);
        if (!response.data?.data) {
          return { notFound: true };
        }
        return {
          props: { page: response.data.data },
          revalidate: 60,
        };
      } catch {
        return { notFound: true };
      }
    };

    export default function LandingPage({ page }: PageProps) {
      return (
        <main>
          <h1>{page.fields.headline}</h1>
          <p>{page.fields.subheadline}</p>
          <div dangerouslySetInnerHTML={{ __html: page.fields.body }} />
        </main>
      );
    }
    ```
  </Tab>
</Tabs>

## Collections

<Tabs>
  <Tab title="App Router">
    ```typescript theme={null}
    // app/brands/page.tsx
    import butter from '@/lib/buttercms';

    interface Brand {
      name: string;
      logo: string;
      description: string;
    }

    async function getBrands(): Promise<Brand[]> {
      const response = await butter.content.retrieve(['brands']);
      if (!response.data?.data) {
        throw new Error('Failed to load brands from ButterCMS');
      }
      return response.data.data.brands;
    }

    export default async function BrandsPage() {
      const brands = await getBrands();

      return (
        <main>
          <h1>Our Brands</h1>
          <ul>
            {brands.map((brand, index) => (
              <li key={index}>
                <img src={brand.logo} alt={brand.name} />
                <h2>{brand.name}</h2>
                <div dangerouslySetInnerHTML={{ __html: brand.description }} />
              </li>
            ))}
          </ul>
        </main>
      );
    }

    export const revalidate = 60;
    ```
  </Tab>

  <Tab title="Pages Router">
    ```typescript theme={null}
    // pages/brands.tsx
    import butter from '@/lib/buttercms';
    import { GetStaticProps } from 'next';

    interface Brand {
      name: string;
      logo: string;
      description: string;
    }

    interface Props {
      brands: Brand[];
    }

    export const getStaticProps: GetStaticProps<Props> = async () => {
      const response = await butter.content.retrieve(['brands']);
      if (!response.data?.data) {
        throw new Error('Failed to load brands from ButterCMS');
      }
      return {
        props: { brands: response.data.data.brands },
        revalidate: 60,
      };
    };

    export default function BrandsPage({ brands }: Props) {
      return (
        <main>
          <h1>Our Brands</h1>
          <ul>
            {brands.map((brand, index) => (
              <li key={index}>
                <img src={brand.logo} alt={brand.name} />
                <h2>{brand.name}</h2>
                <div dangerouslySetInnerHTML={{ __html: brand.description }} />
              </li>
            ))}
          </ul>
        </main>
      );
    }
    ```
  </Tab>
</Tabs>

## Dynamic components

### Component Renderer

```tsx theme={null}
// components/ComponentRenderer.tsx
import Hero from './Hero';
import Features from './Features';
import Testimonials from './Testimonials';
import CTA from './CTA';

export type Component =
  | { type: 'hero'; fields: React.ComponentProps<typeof Hero> }
  | { type: 'features'; fields: React.ComponentProps<typeof Features> }
  | { type: 'testimonials'; fields: React.ComponentProps<typeof Testimonials> }
  | { type: 'cta'; fields: React.ComponentProps<typeof CTA> };

export default function ComponentRenderer({ components }: { components: Component[] }) {
  return (
    <>
      {components.map((component, index) => {
        switch (component.type) {
          case 'hero':
            return <Hero key={index} {...component.fields} />;
          case 'features':
            return <Features key={index} {...component.fields} />;
          case 'testimonials':
            return <Testimonials key={index} {...component.fields} />;
          case 'cta':
            return <CTA key={index} {...component.fields} />;
          default:
            return null;
        }
      })}
    </>
  );
}
```

### Example Component

```tsx theme={null}
// components/Hero.tsx
interface HeroProps {
  headline: string;
  subheadline: string;
  image: string;
  button_label: string;
  button_url: string;
}

export default function Hero({ headline, subheadline, image, button_label, button_url }: HeroProps) {
  return (
    <section className="hero">
      <h1>{headline}</h1>
      <p>{subheadline}</p>
      {button_label && <a href={button_url}>{button_label}</a>}
      {image && <img src={image} alt={headline} />}
    </section>
  );
}
```

### Using in Pages

<Info>
  This uses a distinct page type (`component_page`) whose `body` is a **Component Picker** (Page Builder) field — an array of components — separate from the WYSIWYG `landing_page` in the Pages section (whose `body` is a string). A page's `body` is one field type or the other, so the Page Builder example needs its own page type.
</Info>

<Tabs>
  <Tab title="App Router">
    ```typescript theme={null}
    // app/landing/[slug]/page.tsx
    import butter from '@/lib/buttercms';
    import ComponentRenderer, { type Component } from '@/components/ComponentRenderer';
    import { notFound } from 'next/navigation';

    interface LandingPageFields {
      body: Component[];
    }

    async function getPage(slug: string) {
      try {
        const response = await butter.page.retrieve<LandingPageFields>('component_page', slug);
        return response.data?.data ?? null;
      } catch {
        return null;
      }
    }

    export default async function LandingPage({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params;
      const page = await getPage(slug);
      if (!page) notFound();

      return (
        <main>
          <ComponentRenderer components={page.fields.body} />
        </main>
      );
    }

    export const revalidate = 60;
    ```
  </Tab>

  <Tab title="Pages Router">
    ```typescript theme={null}
    // pages/landing/[slug].tsx
    import butter from '@/lib/buttercms';
    import ComponentRenderer, { type Component } from '@/components/ComponentRenderer';
    import { GetStaticProps, GetStaticPaths } from 'next';

    interface LandingPageFields {
      body: Component[];
    }

    interface Props {
      page: {
        fields: LandingPageFields;
      };
    }

    export const getStaticPaths: GetStaticPaths = async () => {
      const response = await butter.page.list<LandingPageFields>('component_page');
      if (!response.data?.data) {
        throw new Error('Failed to load pages from ButterCMS');
      }
      const paths = response.data.data.map((page) => ({
        params: { slug: page.slug },
      }));
      return { paths, fallback: 'blocking' };
    };

    export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
      try {
        const response = await butter.page.retrieve<LandingPageFields>('component_page', params?.slug as string);
        if (!response.data?.data) {
          return { notFound: true };
        }
        return { props: { page: response.data.data }, revalidate: 60 };
      } catch {
        return { notFound: true };
      }
    };

    export default function LandingPage({ page }: Props) {
      return (
        <main>
          <ComponentRenderer components={page.fields.body} />
        </main>
      );
    }
    ```
  </Tab>
</Tabs>

## Blog

<Tabs>
  <Tab title="Blog Post List">
    <Tabs>
      <Tab title="App Router">
        ```typescript theme={null}
        // app/blog/page.tsx
        import butter from '@/lib/buttercms';
        import Link from 'next/link';

        async function getPosts(page = 1) {
          const response = await butter.post.list({ page, page_size: 10 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          return {
            posts: response.data.data,
            meta: response.data.meta,
          };
        }

        export default async function BlogPage() {
          const { posts, meta } = await getPosts();

          return (
            <main>
              <h1>Blog</h1>
              <ul>
                {posts.map((post) => (
                  <li key={post.slug}>
                    <h2><Link href={`/blog/${post.slug}`}>{post.title}</Link></h2>
                    <p dangerouslySetInnerHTML={{ __html: post.summary ?? '' }} />
                    <span>By {post.author.first_name} {post.author.last_name}</span>
                  </li>
                ))}
              </ul>
              {meta.next_page && (
                <Link href={`/blog?page=${meta.next_page}`}>Next Page</Link>
              )}
            </main>
          );
        }

        export const revalidate = 60;
        ```
      </Tab>

      <Tab title="Pages Router">
        ```typescript theme={null}
        // pages/blog/index.tsx
        import butter from '@/lib/buttercms';
        import Link from 'next/link';
        import { GetStaticProps } from 'next';

        interface Post {
          slug: string;
          title: string;
          summary?: string;
        }

        interface Props {
          posts: Post[];
        }

        export const getStaticProps: GetStaticProps<Props> = async () => {
          const response = await butter.post.list({ page: 1, page_size: 10 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          return {
            props: {
              posts: response.data.data,
            },
            revalidate: 60,
          };
        };

        export default function BlogPage({ posts }: Props) {
          return (
            <main>
              <h1>Blog</h1>
              <ul>
                {posts.map((post) => (
                  <li key={post.slug}>
                    <h2><Link href={`/blog/${post.slug}`}>{post.title}</Link></h2>
                    <p dangerouslySetInnerHTML={{ __html: post.summary ?? '' }} />
                  </li>
                ))}
              </ul>
            </main>
          );
        }
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Single Blog Post">
    <Tabs>
      <Tab title="App Router">
        ```typescript theme={null}
        // app/blog/[slug]/page.tsx
        import butter from '@/lib/buttercms';
        import Link from 'next/link';
        import { notFound } from 'next/navigation';

        export async function generateStaticParams() {
          const response = await butter.post.list({ page_size: 100 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          return response.data.data.map((post) => ({ slug: post.slug }));
        }

        async function getPost(slug: string) {
          try {
            const response = await butter.post.retrieve(slug);
            return response.data?.data ?? null;
          } catch {
            return null;
          }
        }

        export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
          const { slug } = await params;
          const post = await getPost(slug);
          if (!post) notFound();

          return (
            <article>
              <h1>{post.title}</h1>
              <p>
                By {post.author.first_name} {post.author.last_name}
                {post.published && ` on ${new Date(post.published).toLocaleDateString()}`}
              </p>
              {post.featured_image && <img src={post.featured_image} alt={post.title} />}
              <div dangerouslySetInnerHTML={{ __html: post.body ?? '' }} />
              <Link href="/blog">Back to Posts</Link>
            </article>
          );
        }

        export const revalidate = 60;
        ```
      </Tab>

      <Tab title="Pages Router">
        ```typescript theme={null}
        // pages/blog/[slug].tsx
        import butter from '@/lib/buttercms';
        import Link from 'next/link';
        import { GetStaticProps, GetStaticPaths } from 'next';

        interface Post {
          title: string;
          author: { first_name: string; last_name: string };
          body?: string;
        }

        interface Props {
          post: Post;
        }

        export const getStaticPaths: GetStaticPaths = async () => {
          const response = await butter.post.list({ page_size: 100 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          const paths = response.data.data.map((post) => ({
            params: { slug: post.slug },
          }));
          return { paths, fallback: 'blocking' };
        };

        export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
          try {
            const response = await butter.post.retrieve(params?.slug as string);
            if (!response.data?.data) {
              return { notFound: true };
            }
            return { props: { post: response.data.data }, revalidate: 60 };
          } catch {
            return { notFound: true };
          }
        };

        export default function BlogPost({ post }: Props) {
          return (
            <article>
              <h1>{post.title}</h1>
              <p>By {post.author.first_name} {post.author.last_name}</p>
              <div dangerouslySetInnerHTML={{ __html: post.body ?? '' }} />
              <Link href="/blog">Back to Posts</Link>
            </article>
          );
        }
        ```
      </Tab>
    </Tabs>
  </Tab>
</Tabs>

## Preview Mode

Enable content editors to preview draft content before publishing.

<Tabs>
  <Tab title="App Router">
    ```typescript theme={null}
    // app/api/preview/route.ts
    import { draftMode } from 'next/headers';
    import { redirect } from 'next/navigation';

    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const slug = searchParams.get('slug');
      const secret = searchParams.get('secret');

      if (secret !== process.env.PREVIEW_SECRET) {
        return new Response('Invalid token', { status: 401 });
      }

      (await draftMode()).enable();
      redirect(slug || '/');
    }
    ```

    Update your client to use preview mode:

    ```typescript theme={null}
    // lib/buttercms.ts
    import Butter from 'buttercms';
    import { draftMode } from 'next/headers';

    export async function getButterClient() {
      const { isEnabled } = await draftMode();
      return Butter(process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY!, {
        testMode: isEnabled,
      });
    }
    ```
  </Tab>

  <Tab title="Pages Router">
    ```typescript theme={null}
    // pages/api/preview.ts
    import type { NextApiRequest, NextApiResponse } from 'next';

    export default function handler(req: NextApiRequest, res: NextApiResponse) {
      const { secret, slug } = req.query;

      if (secret !== process.env.PREVIEW_SECRET) {
        return res.status(401).json({ message: 'Invalid token' });
      }

      res.setPreviewData({});
      res.redirect(slug as string || '/');
    }
    ```

    Check preview mode in `getStaticProps`:

    ```typescript theme={null}
    export const getStaticProps: GetStaticProps = async ({ preview }) => {
      const butter = Butter(process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY!, {
        testMode: preview,
      });
      // ... fetch content
    };
    ```
  </Tab>
</Tabs>

## SEO

```typescript theme={null}
// app/[slug]/page.tsx
import { Metadata } from 'next';
import butter from '@/lib/buttercms';

interface LandingPageFields {
  headline: string;
  seo?: {
    title?: string;
    description?: string;
    og_title?: string;
    og_description?: string;
    og_image?: string;
  };
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params;
  const response = await butter.page.retrieve<LandingPageFields>('landing_page', slug);
  if (!response.data?.data) {
    throw new Error('Failed to load page from ButterCMS');
  }
  const page = response.data.data;
  const seo = page.fields.seo;

  return {
    title: seo?.title || page.fields.headline,
    description: seo?.description,
    openGraph: {
      title: seo?.og_title || seo?.title,
      description: seo?.og_description || seo?.description,
      images: seo?.og_image ? [seo.og_image] : [],
    },
  };
}
```

## Resources

<CardGroup cols={2}>
  <Card title="Next.js Starter" icon="rocket" href="../starter-projects/nextjs">
    Pre-configured starter project
  </Card>

  <Card title="JavaScript SDK" icon="js" href="../sdks/javascript-sdk">
    Complete SDK reference
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/ButterCMS/nextjs-starter-buttercms">
    View source code
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
