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.

Node.js Starter Project

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

Installation

npm install buttercms
Set your API token as an environment variable:
BUTTERCMS_API_TOKEN=your_api_token

Initialize the client

// lib/buttercms.js
import Butter from 'buttercms';

const butter = Butter(process.env.BUTTERCMS_API_TOKEN);

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

Pages

// services/pages.js
import butter from '../lib/buttercms.js';

export async function getPage(pageType, slug, params = {}) {
  try {
    const response = await butter.page.retrieve(pageType, slug, params);
    return response.data.data;
  } catch (error) {
    console.error('Page not found:', error);
    return null;
  }
}

export async function getPages(pageType, params = {}) {
  const response = await butter.page.list(pageType, params);
  return response.data.data;
}

// Usage
const page = await getPage('landing-page', 'home');
console.log(page.fields.headline);
console.log(page.fields.body);

Collections

// services/collections.js
import butter from '../lib/buttercms.js';

export async function getCollection(keys, params = {}) {
  const response = await butter.content.retrieve(keys, params);
  return response.data.data;
}

// Usage
const content = await getCollection(['brands', 'navigation']);
console.log(content.brands);
console.log(content.navigation);

Dynamic components

Component Renderer

// services/components.js
const componentRenderers = {
  hero: (fields) => ({
    type: 'hero',
    html: `
      <section class="hero">
        <h1>${fields.headline}</h1>
        <p>${fields.subheadline}</p>
        ${fields.button_label ? `<a href="${fields.button_url}" class="btn">${fields.button_label}</a>` : ''}
        ${fields.image ? `<img src="${fields.image}" alt="${fields.headline}" />` : ''}
      </section>
    `
  }),

  features: (fields) => ({
    type: 'features',
    html: `
      <section class="features">
        <h2>${fields.headline}</h2>
        <div class="features-grid">
          ${fields.items.map(item => `
            <div class="feature">
              ${item.icon ? `<img src="${item.icon}" alt="${item.title}" />` : ''}
              <h3>${item.title}</h3>
              <p>${item.description}</p>
            </div>
          `).join('')}
        </div>
      </section>
    `
  }),

  cta: (fields) => ({
    type: 'cta',
    html: `
      <section class="cta">
        <h2>${fields.headline}</h2>
        <p>${fields.subheadline}</p>
        <a href="${fields.button_url}" class="btn">${fields.button_label}</a>
      </section>
    `
  })
};

export function renderComponents(components) {
  return components.map(component => {
    const renderer = componentRenderers[component.type];
    if (renderer) {
      return renderer(component.fields);
    }
    console.warn(`Unknown component type: ${component.type}`);
    return { type: component.type, html: '' };
  });
}

export function renderComponentsToHTML(components) {
  return renderComponents(components).map(c => c.html).join('');
}

Example Component

// Example hero component renderer
function renderHero(fields) {
  return `
    <section class="hero">
      <div class="hero-content">
        <h1>${fields.headline}</h1>
        <p class="subheadline">${fields.subheadline}</p>
        ${fields.button_label ? `
          <a href="${fields.button_url}" class="hero-cta">${fields.button_label}</a>
        ` : ''}
      </div>
      ${fields.image ? `
        <div class="hero-image">
          <img src="${fields.image}" alt="${fields.headline}" />
        </div>
      ` : ''}
    </section>
  `;
}

Using in Pages

// services/component-pages.js
import butter from '../lib/buttercms.js';
import { renderComponentsToHTML } from './components.js';

export async function getComponentPage(slug) {
  try {
    const response = await butter.page.retrieve('landing-page', slug);
    const page = response.data.data;

    // Render body components to HTML
    const bodyComponents = page.fields.body || [];
    const renderedHTML = renderComponentsToHTML(bodyComponents);

    return {
      page,
      renderedHTML
    };
  } catch (error) {
    console.error('Page not found:', error);
    return null;
  }
}

// Usage
const { page, renderedHTML } = await getComponentPage('home');
// Use renderedHTML in your template

Blog

// services/blog.js
import butter from '../lib/buttercms.js';

export async function getPosts(params = {}) {
  const response = await butter.post.list({
    page: 1,
    page_size: 10,
    ...params
  });
  return {
    posts: response.data.data,
    meta: response.data.meta
  };
}

export async function getPostsByCategory(categorySlug, params = {}) {
  return getPosts({
    category_slug: categorySlug,
    ...params
  });
}

export async function searchPosts(query, params = {}) {
  const response = await butter.post.search(query, params);
  return response.data.data;
}

// Usage
const { posts, meta } = await getPosts({ page: 1 });
posts.forEach(post => {
  console.log(post.title);
  console.log(`By ${post.author.first_name} ${post.author.last_name}`);
});

if (meta.next_page) {
  const nextPage = await getPosts({ page: meta.next_page });
}

Preview Mode

// services/preview.js
import butter from '../lib/buttercms.js';

export async function getPageWithPreview(pageType, slug, preview = false) {
  const params = {};
  if (preview) {
    params.preview = 1;
  }

  try {
    const response = await butter.page.retrieve(pageType, slug, params);
    return response.data.data;
  } catch (error) {
    return null;
  }
}

// Usage in HTTP handler
export function handlePageRequest(req, res) {
  const { slug } = req.params;
  const preview = req.query.preview === 'true';

  const page = await getPageWithPreview('landing-page', slug, preview);

  if (!page) {
    return res.status(404).json({ error: 'Page not found' });
  }

  res.json({ page, isPreview: preview });
}

Caching

// services/cache.js
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 3600 }); // 1 hour TTL

export function getCached(key) {
  return cache.get(key);
}

export function setCache(key, value, ttl) {
  cache.set(key, value, ttl);
}

export function invalidateCache(pattern) {
  const keys = cache.keys();
  keys.forEach(key => {
    if (key.includes(pattern)) {
      cache.del(key);
    }
  });
}

// Usage with ButterCMS
import butter from '../lib/buttercms.js';

export async function getPageCached(pageType, slug, preview = false) {
  // Skip cache for preview mode
  if (preview) {
    const response = await butter.page.retrieve(pageType, slug, { preview: 1 });
    return response.data.data;
  }

  const cacheKey = `page:${pageType}:${slug}`;
  let page = getCached(cacheKey);

  if (!page) {
    const response = await butter.page.retrieve(pageType, slug);
    page = response.data.data;
    setCache(cacheKey, page);
  }

  return page;
}

export async function getPostsCached(params = {}) {
  const cacheKey = `posts:${JSON.stringify(params)}`;
  let result = getCached(cacheKey);

  if (!result) {
    const response = await butter.post.list(params);
    result = {
      posts: response.data.data,
      meta: response.data.meta
    };
    setCache(cacheKey, result);
  }

  return result;
}

Webhook cache invalidation

// webhooks/butter.js
import { invalidateCache } from '../services/cache.js';

export function handleWebhook(req, res) {
  const { webhook } = req.body;
  const event = webhook?.event || '';

  if (event.includes('page')) {
    invalidateCache('page:');
  }

  if (event.includes('post')) {
    invalidateCache('posts:');
  }

  if (event.includes('content')) {
    invalidateCache('collection:');
  }

  res.status(200).json({ status: 'ok' });
}

SEO

// services/seo.js
export function extractSEO(page) {
  const seo = page.fields.seo || {};
  const headline = page.fields.headline || '';

  return {
    title: seo.title || headline,
    description: seo.description || '',
    ogTitle: seo.og_title || seo.title || headline,
    ogDescription: seo.og_description || seo.description || '',
    ogImage: seo.og_image || null
  };
}

export function generateMetaTags(seo) {
  const tags = [
    `<title>${seo.title}</title>`,
    `<meta name="description" content="${seo.description}">`,
    `<meta property="og:title" content="${seo.ogTitle}">`,
    `<meta property="og:description" content="${seo.ogDescription}">`
  ];

  if (seo.ogImage) {
    tags.push(`<meta property="og:image" content="${seo.ogImage}">`);
  }

  tags.push(
    `<meta name="twitter:card" content="summary_large_image">`,
    `<meta name="twitter:title" content="${seo.title}">`,
    `<meta name="twitter:description" content="${seo.description}">`
  );

  return tags.join('\n');
}

// Usage
const page = await getPage('landing-page', 'home');
const seo = extractSEO(page);
const metaTags = generateMetaTags(seo);

HTTP server example

// server.js
import http from 'http';
import butter from './lib/buttercms.js';
import { getPageCached, getPostsCached, invalidateCache } from './services/cache.js';

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  res.setHeader('Content-Type', 'application/json');

  try {
    // Pages
    if (url.pathname.match(/^\/api\/page\/(.+)\/(.+)$/)) {
      const [, pageType, slug] = url.pathname.match(/^\/api\/page\/(.+)\/(.+)$/);
      const preview = url.searchParams.get('preview') === 'true';
      const page = await getPageCached(pageType, slug, preview);
      return res.end(JSON.stringify(page));
    }

    // Blog posts list
    if (url.pathname === '/api/posts') {
      const page = parseInt(url.searchParams.get('page') || '1');
      const result = await getPostsCached({ page, page_size: 10 });
      return res.end(JSON.stringify(result));
    }

    // Single blog post
    if (url.pathname.match(/^\/api\/posts\/(.+)$/)) {
      const [, slug] = url.pathname.match(/^\/api\/posts\/(.+)$/);
      const response = await butter.post.retrieve(slug);
      return res.end(JSON.stringify(response.data.data));
    }

    // Collections
    if (url.pathname === '/api/content') {
      const keys = url.searchParams.get('keys')?.split(',') || [];
      const response = await butter.content.retrieve(keys);
      return res.end(JSON.stringify(response.data.data));
    }

    // Webhook
    if (url.pathname === '/webhooks/butter' && req.method === 'POST') {
      let body = '';
      req.on('data', chunk => body += chunk);
      req.on('end', () => {
        const { webhook } = JSON.parse(body);
        if (webhook?.event?.includes('page')) invalidateCache('page:');
        if (webhook?.event?.includes('post')) invalidateCache('posts:');
        res.end(JSON.stringify({ status: 'ok' }));
      });
      return;
    }

    res.statusCode = 404;
    res.end(JSON.stringify({ error: 'Not found' }));
  } catch (error) {
    res.statusCode = error.response?.status || 500;
    res.end(JSON.stringify({ error: error.message }));
  }
});

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Resources

Node.js Starter

Pre-configured starter project

JavaScript SDK

Complete SDK reference

Express.js Guide

Express.js integration

Webhooks

Set up content webhooks