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

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: { slug: string };
}

export async function generateStaticParams() {
  const response = await butter.page.list('landing-page');
  return response.data.data.map((page) => ({
    slug: page.slug,
  }));
}

async function getPage(slug: string) {
  try {
    const response = await butter.page.retrieve('landing-page', slug);
    return response.data.data;
  } catch {
    return null;
  }
}

export default async function LandingPage({ params }: PageProps) {
  const page = await getPage(params.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']);
  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';

interface Component {
  type: string;
  fields: Record<string, any>;
}

const componentMap: Record<string, React.ComponentType<any>> = {
  hero: Hero,
  features: Features,
  testimonials: Testimonials,
  cta: CTA,
};

export default function ComponentRenderer({ components }: { components: Component[] }) {
  return (
    <>
      {components.map((component, index) => {
        const Component = componentMap[component.type];
        if (!Component) return null;
        return <Component key={index} {...component.fields} />;
      })}
    </>
  );
}

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

// app/landing/[slug]/page.tsx
import butter from '@/lib/buttercms';
import ComponentRenderer from '@/components/ComponentRenderer';
import { notFound } from 'next/navigation';

async function getPage(slug: string) {
  try {
    const response = await butter.page.retrieve('landing-page', slug);
    return response.data.data;
  } catch {
    return null;
  }
}

export default async function LandingPage({ params }: { params: { slug: string } }) {
  const page = await getPage(params.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 });
  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 });
  }

  draftMode().enable();
  redirect(slug || '/');
}
Update your client to use preview mode:
// lib/buttercms.ts
import Butter from 'buttercms';
import { draftMode } from 'next/headers';

export function getButterClient() {
  const { isEnabled } = 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';

export async function generateMetadata({ params }): Promise<Metadata> {
  const response = await butter.page.retrieve('landing-page', params.slug);
  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