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
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 (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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;
// pages/[slug].tsx
import butter from '@/lib/buttercms';
import { GetStaticProps, GetStaticPaths } from 'next';
interface PageProps {
page: {
fields: {
headline: string;
subheadline: string;
hero_image: string;
body: string;
};
};
}
export const getStaticPaths: GetStaticPaths = async () => {
const response = await butter.page.list('landing-page');
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('landing-page', params?.slug as string);
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 (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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;
// 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']);
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';
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 Router (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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;
// pages/landing/[slug].tsx
import butter from '@/lib/buttercms';
import ComponentRenderer from '@/components/ComponentRenderer';
import { GetStaticProps, GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async () => {
const response = await butter.page.list('landing-page');
const paths = response.data.data.map((page) => ({
params: { slug: page.slug },
}));
return { paths, fallback: 'blocking' };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const response = await butter.page.retrieve('landing-page', params?.slug as string);
return { props: { page: response.data.data }, revalidate: 60 };
} catch {
return { notFound: true };
}
};
export default function LandingPage({ page }) {
return (
<main>
<ComponentRenderer components={page.fields.body} />
</main>
);
}
Blog
- Blog Post List
- Single Blog Post
- App Router (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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;
// pages/blog/index.tsx
import butter from '@/lib/buttercms';
import Link from 'next/link';
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async () => {
const response = await butter.post.list({ page: 1, page_size: 10 });
return {
props: {
posts: response.data.data,
meta: response.data.meta,
},
revalidate: 60,
};
};
export default function BlogPage({ posts, meta }) {
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 (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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 });
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;
} catch {
return null;
}
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>
By {post.author.first_name} {post.author.last_name} 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';
export const getStaticPaths: GetStaticPaths = async () => {
const response = await butter.post.list({ page_size: 100 });
const paths = response.data.data.map((post) => ({
params: { slug: post.slug },
}));
return { paths, fallback: 'blocking' };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const response = await butter.post.retrieve(params?.slug as string);
return { props: { post: response.data.data }, revalidate: 60 };
} catch {
return { notFound: true };
}
};
export default function BlogPost({ post }) {
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 (Next.js 13+)
- Pages Router (Next.js 12 and earlier)
// 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 || '/');
}
// 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,
});
}
// 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';
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