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.

React Starter Project

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

Installation

npm install buttercms
Add your API token to .env:
VITE_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(import.meta.env.VITE_BUTTER_CMS_API_KEY);

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

Pages

// 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();
  const [page, setPage] = useState<{ fields: PageFields } | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!slug) {
      setError('Page not found');
      setLoading(false);
      return;
    }
    async function fetchPage(pageSlug: string) {
      try {
        const response = await butter.page.retrieve<PageFields>('landing_page', pageSlug);
        setPage(response.data?.data ?? null);
      } catch (err) {
        setError('Page not found');
      } finally {
        setLoading(false);
      }
    }
    fetchPage(slug);
  }, [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>
  );
}
Router setup:
// 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>
  );
}

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']);
        if (!response.data?.data) {
          throw new Error('Failed to load brands from ButterCMS');
        }
        const data = response.data.data as { brands: Brand[] };
        setBrands(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';

export type PageComponent =
  | { 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: PageComponent[] }) {
  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

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

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.
// src/pages/LandingPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import butter from '../lib/buttercms';
import ComponentRenderer, { type PageComponent } from '../components/ComponentRenderer';

interface PageFields {
  body: PageComponent[];
}

export default function LandingPage() {
  const { slug } = useParams();
  const [page, setPage] = useState<{ fields: PageFields } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!slug) {
      setLoading(false);
      return;
    }
    async function fetchPage(pageSlug: string) {
      try {
        const response = await butter.page.retrieve<PageFields>('component_page', pageSlug);
        setPage(response.data?.data ?? null);
      } finally {
        setLoading(false);
      }
    }
    fetchPage(slug);
  }, [slug]);

  if (loading) return <div>Loading...</div>;
  if (!page) return <div>Page not found</div>;

  return (
    <main>
      <ComponentRenderer components={page.fields.body} />
    </main>
  );
}

Blog

// 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 });
        if (!response.data?.data) {
          throw new Error('Failed to load posts from ButterCMS');
        }
        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>
  );
}

SEO with React Helmet

// src/pages/LandingPage.tsx
import { Helmet } from 'react-helmet-async';
import { usePage } from '../hooks/usePage';

interface Seo {
  title?: string;
  description?: string;
  og_title?: string;
  og_description?: string;
  og_image?: string;
}

interface PageFields {
  headline: string;
  body: string | null;
  seo?: Seo;
}

export default function LandingPage({ slug }: { slug: string }) {
  const { page } = usePage<PageFields>('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