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.Node.js Starter Project
Hit the ground running with a pre-configured Node.js + ButterCMS setup.
Installation
- npm
- yarn
- pnpm
npm install buttercms
yarn add buttercms
pnpm add buttercms
BUTTERCMS_API_TOKEN=your_api_token
Initialize the client
- ES Modules
- CommonJS
- TypeScript
// lib/buttercms.js
import Butter from 'buttercms';
const butter = Butter(process.env.BUTTERCMS_API_TOKEN);
export default butter;
// lib/buttercms.js
const Butter = require('buttercms');
const butter = Butter(process.env.BUTTERCMS_API_TOKEN);
module.exports = butter;
// lib/buttercms.ts
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
- ES Modules
- CommonJS
- TypeScript
// 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);
// services/pages.js
const butter = require('../lib/buttercms');
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;
}
}
async function getPages(pageType, params = {}) {
const response = await butter.page.list(pageType, params);
return response.data.data;
}
module.exports = { getPage, getPages };
// services/pages.ts
import butter from '../lib/buttercms';
interface PageFields {
headline: string;
subheadline: string;
hero_image: string;
body: string;
seo?: {
title: string;
description: string;
og_title?: string;
og_image?: string;
};
}
interface Page<T = PageFields> {
slug: string;
fields: T;
}
export async function getPage<T = PageFields>(
pageType: string,
slug: string,
params: Record<string, string> = {}
): Promise<Page<T> | null> {
try {
const response = await butter.page.retrieve(pageType, slug, params);
return response.data.data;
} catch {
return null;
}
}
export async function getPages<T = PageFields>(
pageType: string,
params: Record<string, string> = {}
): Promise<Page<T>[]> {
const response = await butter.page.list(pageType, params);
return response.data.data;
}
Collections
- ES Modules
- CommonJS
- TypeScript
// 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);
// services/collections.js
const butter = require('../lib/buttercms');
async function getCollection(keys, params = {}) {
const response = await butter.content.retrieve(keys, params);
return response.data.data;
}
module.exports = { getCollection };
// services/collections.ts
import butter from '../lib/buttercms';
interface Brand {
name: string;
logo: string;
description: string;
}
interface CollectionResponse {
brands?: Brand[];
navigation?: Array<{ label: string; url: string }>;
[key: string]: unknown;
}
export async function getCollection(
keys: string[],
params: Record<string, string> = {}
): Promise<CollectionResponse> {
const response = await butter.content.retrieve(keys, params);
return response.data.data;
}
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
- ES Modules
- CommonJS
- TypeScript
// 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
// services/component-pages.js
const butter = require('../lib/buttercms');
const { renderComponentsToHTML } = require('./components');
async function getComponentPage(slug) {
try {
const response = await butter.page.retrieve('landing-page', slug);
const page = response.data.data;
const bodyComponents = page.fields.body || [];
const renderedHTML = renderComponentsToHTML(bodyComponents);
return { page, renderedHTML };
} catch (error) {
console.error('Page not found:', error);
return null;
}
}
module.exports = { getComponentPage };
// services/component-pages.ts
import butter from '../lib/buttercms';
import { renderComponentsToHTML } from './components';
interface ComponentField {
type: string;
fields: Record<string, unknown>;
}
interface ComponentPageFields {
body: ComponentField[];
seo?: {
title: string;
description: string;
};
}
export async function getComponentPage(slug: string): Promise<{
page: { slug: string; fields: ComponentPageFields };
renderedHTML: string;
} | null> {
try {
const response = await butter.page.retrieve('landing-page', slug);
const page = response.data.data;
const bodyComponents = page.fields.body || [];
const renderedHTML = renderComponentsToHTML(bodyComponents);
return { page, renderedHTML };
} catch {
return null;
}
}
Blog
- Blog List
- Blog Post
- ES Modules
- CommonJS
- TypeScript
// 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 });
}
// services/blog.js
const butter = require('../lib/buttercms');
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
};
}
async function getPostsByCategory(categorySlug, params = {}) {
return getPosts({
category_slug: categorySlug,
...params
});
}
module.exports = { getPosts, getPostsByCategory };
// services/blog.ts
import butter from '../lib/buttercms';
interface Post {
slug: string;
title: string;
body: string;
summary: string;
published: string;
featured_image?: string;
seo_title?: string;
meta_description?: string;
author: {
first_name: string;
last_name: string;
email?: string;
bio?: string;
};
categories: Array<{ name: string; slug: string }>;
tags: Array<{ name: string; slug: string }>;
}
interface PostsMeta {
next_page: number | null;
previous_page: number | null;
count: number;
}
export async function getPosts(
params: Record<string, unknown> = {}
): Promise<{ posts: Post[]; meta: PostsMeta }> {
const response = await butter.post.list({
page: 1,
page_size: 10,
...params
});
return {
posts: response.data.data,
meta: response.data.meta
};
}
- ES Modules
- CommonJS
- TypeScript
// services/blog.js
import butter from '../lib/buttercms.js';
export async function getPost(slug) {
try {
const response = await butter.post.retrieve(slug);
return response.data.data;
} catch (error) {
console.error('Post not found:', error);
return null;
}
}
// Usage
const post = await getPost('my-first-post');
if (post) {
console.log(post.title);
console.log(post.body);
console.log(`By ${post.author.first_name} ${post.author.last_name}`);
}
// services/blog.js
const butter = require('../lib/buttercms');
async function getPost(slug) {
try {
const response = await butter.post.retrieve(slug);
return response.data.data;
} catch (error) {
console.error('Post not found:', error);
return null;
}
}
module.exports = { getPost };
// services/blog.ts
import butter from '../lib/buttercms';
export async function getPost(slug: string): Promise<Post | null> {
try {
const response = await butter.post.retrieve(slug);
return response.data.data;
} catch {
return null;
}
}
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