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.React Starter Project
Hit the ground running with a pre-configured React + ButterCMS setup.
Installation
- npm
- yarn
- pnpm
npm install buttercms
yarn add buttercms
pnpm add buttercms
.env:
REACT_APP_BUTTER_CMS_API_KEY=your_api_token
Initialize the client
Create a reusable client instance:// src/lib/buttercms.ts
import Butter from 'buttercms';
const butter = Butter(process.env.REACT_APP_BUTTER_CMS_API_KEY!);
export default butter;
For complete SDK documentation including all available methods and configuration options, see the JavaScript SDK Reference.
Pages
- React Router
- Custom Hook
// src/pages/LandingPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import butter from '../lib/buttercms';
interface PageFields {
headline: string;
subheadline: string;
hero_image: string;
body: string;
}
export default function LandingPage() {
const { slug } = useParams<{ slug: string }>();
const [page, setPage] = useState<{ fields: PageFields } | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchPage() {
try {
const response = await butter.page.retrieve('landing-page', slug!);
setPage(response.data.data);
} catch (err) {
setError('Page not found');
} finally {
setLoading(false);
}
}
fetchPage();
}, [slug]);
if (loading) return <div>Loading...</div>;
if (error || !page) return <div>{error || 'Page not found'}</div>;
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>
);
}
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LandingPage from './pages/LandingPage';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/:slug" element={<LandingPage />} />
</Routes>
</BrowserRouter>
);
}
// src/hooks/usePage.ts
import { useEffect, useState } from 'react';
import butter from '../lib/buttercms';
interface UsePageResult<T> {
page: T | null;
loading: boolean;
error: string | null;
}
export function usePage<T>(pageType: string, slug: string): UsePageResult<T> {
const [page, setPage] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchPage() {
try {
const response = await butter.page.retrieve(pageType, slug);
setPage(response.data.data);
} catch (err) {
setError('Page not found');
} finally {
setLoading(false);
}
}
fetchPage();
}, [pageType, slug]);
return { page, loading, error };
}
// Usage in component
// src/pages/LandingPage.tsx
import { useParams } from 'react-router-dom';
import { usePage } from '../hooks/usePage';
interface PageData {
fields: {
headline: string;
body: string;
};
}
export default function LandingPage() {
const { slug } = useParams<{ slug: string }>();
const { page, loading, error } = usePage<PageData>('landing-page', slug!);
if (loading) return <div>Loading...</div>;
if (error || !page) return <div>{error}</div>;
return (
<main>
<h1>{page.fields.headline}</h1>
<div dangerouslySetInnerHTML={{ __html: page.fields.body }} />
</main>
);
}
Collections
// src/pages/BrandsPage.tsx
import { useEffect, useState } from 'react';
import butter from '../lib/buttercms';
interface Brand {
name: string;
logo: string;
description: string;
}
export default function BrandsPage() {
const [brands, setBrands] = useState<Brand[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchBrands() {
try {
const response = await butter.content.retrieve(['brands']);
setBrands(response.data.data.brands);
} finally {
setLoading(false);
}
}
fetchBrands();
}, []);
if (loading) return <div>Loading...</div>;
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
// src/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
// src/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
// src/pages/LandingPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import butter from '../lib/buttercms';
import ComponentRenderer from '../components/ComponentRenderer';
export default function LandingPage() {
const { slug } = useParams<{ slug: string }>();
const [page, setPage] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPage() {
try {
const response = await butter.page.retrieve('landing-page', slug!);
setPage(response.data.data);
} finally {
setLoading(false);
}
}
fetchPage();
}, [slug]);
if (loading) return <div>Loading...</div>;
if (!page) return <div>Page not found</div>;
return (
<main>
<ComponentRenderer components={page.fields.body} />
</main>
);
}
Blog
- Blog Post List
- Single Blog Post
// src/pages/BlogList.tsx
import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import butter from '../lib/buttercms';
interface Post {
slug: string;
title: string;
summary: string;
author: { first_name: string; last_name: string };
}
interface Meta {
next_page: number | null;
previous_page: number | null;
}
export default function BlogList() {
const [posts, setPosts] = useState<Post[]>([]);
const [meta, setMeta] = useState<Meta | null>(null);
const [loading, setLoading] = useState(true);
const [searchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '1');
useEffect(() => {
async function fetchPosts() {
setLoading(true);
try {
const response = await butter.post.list({ page, page_size: 10 });
setPosts(response.data.data);
setMeta(response.data.meta);
} finally {
setLoading(false);
}
}
fetchPosts();
}, [page]);
if (loading) return <div>Loading...</div>;
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<h2><Link to={`/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 to={`/blog?page=${meta.next_page}`}>Next Page</Link>
)}
</main>
);
}
// src/pages/BlogPost.tsx
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import butter from '../lib/buttercms';
interface Post {
title: string;
body: string;
published: string;
featured_image: string;
author: { first_name: string; last_name: string };
}
export default function BlogPost() {
const { slug } = useParams<{ slug: string }>();
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchPost() {
try {
const response = await butter.post.retrieve(slug!);
setPost(response.data.data);
} catch {
setError('Post not found');
} finally {
setLoading(false);
}
}
fetchPost();
}, [slug]);
if (loading) return <div>Loading...</div>;
if (error || !post) return <div>{error || 'Post not found'}</div>;
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 to="/blog">Back to Posts</Link>
</article>
);
}
SEO with React Helmet
// src/pages/LandingPage.tsx
import { Helmet } from 'react-helmet-async';
import { usePage } from '../hooks/usePage';
export default function LandingPage({ slug }: { slug: string }) {
const { page } = usePage<any>('landing-page', slug);
if (!page) return null;
const seo = page.fields.seo || {};
return (
<>
<Helmet>
<title>{seo.title || page.fields.headline}</title>
<meta name="description" content={seo.description} />
<meta property="og:title" content={seo.og_title || seo.title} />
<meta property="og:description" content={seo.og_description || seo.description} />
{seo.og_image && <meta property="og:image" content={seo.og_image} />}
</Helmet>
<main>
<h1>{page.fields.headline}</h1>
<div dangerouslySetInnerHTML={{ __html: page.fields.body }} />
</main>
</>
);
}
Resources
React Starter
Pre-configured starter project
JavaScript SDK
Complete SDK reference
GitHub Repository
View source code
Content API
REST API documentation