Overview
This integration guide shows you how to how to update your existing project to:- install the ButterCMS package
- instantiate ButterCMS
- 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
- yarn
- pnpm
npm install buttercms
yarn add buttercms
pnpm add buttercms
.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 Router
- Pages Router
// 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;
// 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>
);
}
Collections
- App Router
- Pages Router
// 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;
// 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>
);
}
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 Router
- Pages Router
// 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;
// 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>
);
}
Blog
- Blog Post List
- Single Blog Post
- App Router
- Pages Router
// 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;
// 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>
);
}
- App Router
- Pages Router
// 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;
// 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>
);
}
Preview Mode
Enable content editors to preview draft content before publishing.- App Router
- Pages Router
// 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 || '/');
}
// 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,
});
}
// 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 || '/');
}
getStaticProps:export const getStaticProps: GetStaticProps = async ({ preview }) => {
const butter = Butter(process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY!, {
testMode: preview,
});
// ... fetch content
};
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