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.

Astro Starter Project

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

Installation

npm install buttercms
Add your API token to .env:
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.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/[slug].astro
import butter from '../lib/buttercms';

interface PageFields {
  headline: string;
  subheadline: string;
  hero_image: string;
  body: string;
}

export async function getStaticPaths() {
  const response = await butter.page.list<PageFields>('landing_page');
  if (!response.data?.data) {
    throw new Error('Failed to load landing pages from ButterCMS');
  }
  return response.data.data.map((page) => ({
    params: { slug: page.slug },
    props: { page },
  }));
}

interface Props {
  page: {
    fields: PageFields;
  };
}

const { page } = Astro.props;
---

<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 set:html={page.fields.body ?? ''} />
</main>

Collections

---
// src/pages/brands.astro
import butter from '../lib/buttercms';

interface Brand {
  name: string;
  logo: string;
  description: string;
}

const response = await butter.content.retrieve(['brands']);
if (!response.data?.data) {
  throw new Error('Failed to load brands from ButterCMS');
}
const brands: Brand[] = response.data.data.brands;
---

<main>
  <h1>Our Brands</h1>
  <ul>
    {brands.map((brand) => (
      <li>
        <img src={brand.logo} alt={brand.name} />
        <h2>{brand.name}</h2>
        <div set:html={brand.description} />
      </li>
    ))}
  </ul>
</main>

Dynamic components

Component Renderer

---
// src/components/ComponentRenderer.astro
import Hero from './Hero.astro';
import Features from './Features.astro';
import Testimonials from './Testimonials.astro';
import CTA from './CTA.astro';

export interface HeroFields {
  headline: string;
  subheadline: string;
  image: string;
  button_label: string;
  button_url: string;
}

export interface FeaturesFields {
  headline: string;
  items: { icon?: string; title: string; description: string }[];
}

export interface TestimonialsFields {
  headline: string;
  items: { quote: string; author_name: string; author_title?: string }[];
}

export interface CtaFields {
  headline: string;
  subheadline: string;
  button_label: string;
  button_url: string;
}

export type ButterComponent =
  | { type: 'hero'; fields: HeroFields }
  | { type: 'features'; fields: FeaturesFields }
  | { type: 'testimonials'; fields: TestimonialsFields }
  | { type: 'cta'; fields: CtaFields };

const componentMap = {
  hero: Hero,
  features: Features,
  testimonials: Testimonials,
  cta: CTA,
};

interface Props {
  components: ButterComponent[];
}

const { components } = Astro.props;
---

{components.map((component) => {
  const Component = componentMap[component.type];
  return Component ? <Component {...component.fields} /> : null;
})}

Example Component

---
// src/components/Hero.astro
interface Props {
  headline: string;
  subheadline: string;
  image: string;
  button_label: string;
  button_url: string;
}

const { headline, subheadline, image, button_label, button_url } = Astro.props;
---

<section class="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/landing/[slug].astro
import butter from '../../lib/buttercms';
import ComponentRenderer from '../../components/ComponentRenderer.astro';
import type { ButterComponent } from '../../components/ComponentRenderer.astro';

interface PageFields {
  body: ButterComponent[];
}

export async function getStaticPaths() {
  const response = await butter.page.list<PageFields>('component_page');
  if (!response.data?.data) {
    throw new Error('Failed to load landing pages from ButterCMS');
  }
  return response.data.data.map((page) => ({
    params: { slug: page.slug },
    props: { page },
  }));
}

interface Props {
  page: {
    fields: PageFields;
  };
}

const { page } = Astro.props;
---

<main>
  <ComponentRenderer components={page.fields.body} />
</main>

Blog

---
// src/pages/blog/index.astro
import butter from '../../lib/buttercms';

interface Post {
  slug: string;
  title: string;
  summary: string;
  author: { first_name: string; last_name: string };
}

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');
}
const posts: Post[] = response.data.data;
const meta = response.data.meta;
---

<main>
  <h1>Blog</h1>
  <ul>
    {posts.map((post) => (
      <li>
        <h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
        <p set:html={post.summary ?? ''} />
        <span>By {post.author.first_name} {post.author.last_name}</span>
      </li>
    ))}
  </ul>
  {meta?.next_page && (
    <a href={`/blog/page/${meta.next_page}`}>Next Page</a>
  )}
</main>

Preview Mode

Enable draft content preview with SSR:
---
// src/pages/preview/[...slug].astro
import Butter from 'buttercms';

const butter = Butter(import.meta.env.BUTTER_CMS_API_KEY, true); // Enable preview mode

interface PageFields {
  headline: string;
  body: string;
}

const { slug } = Astro.params;
if (!slug) {
  return Astro.redirect('/404');
}
const [pageType, pageSlug] = slug.split('/');

let page: { fields: PageFields };
try {
  const response = await butter.page.retrieve<PageFields>(pageType, pageSlug);
  if (!response.data?.data) {
    return Astro.redirect('/404');
  }
  page = response.data.data;
} catch {
  return Astro.redirect('/404');
}
---

<div class="preview-banner">Preview Mode</div>
<main>
  <h1>{page.fields.headline}</h1>
  <div set:html={page.fields.body ?? ''} />
</main>

SEO

---
// src/pages/[slug].astro
import butter from '../lib/buttercms';
import Layout from '../layouts/Layout.astro';

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

interface PageFields {
  headline: string;
  body: string;
  seo?: SeoFields;
}

export async function getStaticPaths() {
  const response = await butter.page.list<PageFields>('landing_page');
  if (!response.data?.data) {
    throw new Error('Failed to load landing pages from ButterCMS');
  }
  return response.data.data.map((page) => ({
    params: { slug: page.slug },
    props: { page },
  }));
}

interface Props {
  page: {
    fields: PageFields;
  };
}

const { page } = Astro.props;
const seo: SeoFields = page.fields.seo ?? {};
---

<Layout
  title={seo.title || page.fields.headline}
  description={seo.description}
  ogTitle={seo.og_title || seo.title}
  ogDescription={seo.og_description || seo.description}
  ogImage={seo.og_image}
>
  <main>
    <h1>{page.fields.headline}</h1>
    <div set:html={page.fields.body ?? ''} />
  </main>
</Layout>
Layout with SEO:
---
// src/layouts/Layout.astro
interface Props {
  title: string;
  description?: string;
  ogTitle?: string;
  ogDescription?: string;
  ogImage?: string;
}

const { title, description, ogTitle, ogDescription, ogImage } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
    {description && <meta name="description" content={description} />}
    {ogTitle && <meta property="og:title" content={ogTitle} />}
    {ogDescription && <meta property="og:description" content={ogDescription} />}
    {ogImage && <meta property="og:image" content={ogImage} />}
  </head>
  <body>
    <slot />
  </body>
</html>

Resources

Astro Starter

Pre-configured starter project

JavaScript SDK

Complete SDK reference

GitHub Repository

View source code

Content API

REST API documentation