Skip to main content

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, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.

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.

Next.js Starter Project

Hit the ground running with a pre-configured Next.js + ButterCMS setup.

Installation

npm install buttercms
Add your API token to .env.local:
NEXT_PUBLIC_BUTTER_CMS_API_KEY=your_api_token
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).

Initialize the client

Create a reusable client instance:
// lib/buttercms.ts
import Butter from 'buttercms';

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

export default butter;
For complete SDK documentation including all available methods and configuration options, see the JavaScript SDK Reference.

Pages

// 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;

Collections

// 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;

Dynamic components

Component Renderer

// 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

// 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

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.
// 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;

Blog

// 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;

Preview Mode

Enable content editors to preview draft content before publishing.
// 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:
// 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,
  });
}

SEO

// 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

Next.js Starter

Pre-configured starter project

JavaScript SDK

Complete SDK reference

GitHub Repository

View source code

Content API

REST API documentation