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.Express.js Starter Project
Hit the ground running with a pre-configured Express.js + ButterCMS setup.
Installation
- npm
- yarn
- pnpm
npm install buttercms express ejs
yarn add buttercms express ejs
pnpm add buttercms express ejs
.env:
BUTTERCMS_API_TOKEN=your_api_token
Initialize the client
Create a reusable client instance:- JavaScript
- TypeScript
// lib/buttercms.js
const Butter = require('buttercms').default;
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.
Basic setup
- JavaScript
- TypeScript
// app.js
require('dotenv').config();
const express = require('express');
const butter = require('./lib/buttercms');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.listen(3000, () => {
console.log('Server running on port 3000');
});
module.exports = app;
// app.ts
import 'dotenv/config';
import express from 'express';
import butter from './lib/buttercms';
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.listen(3000, () => {
console.log('Server running on port 3000');
});
export default app;
Pages
- JavaScript
- TypeScript
// routes/pages.js
const express = require('express');
const router = express.Router();
const butter = require('../lib/buttercms');
router.get('/', (req, res) => renderPage('home', res));
router.get('/:slug', (req, res) => renderPage(req.params.slug, res));
async function renderPage(slug, res) {
try {
const response = await butter.page.retrieve('landing_page', slug);
if (!response.data?.data) {
throw new Error('Failed to load page from ButterCMS');
}
res.render('landing', { page: response.data.data });
} catch (err) {
res.status(404).render('404');
}
}
module.exports = router;
// routes/pages.ts
import { Router, Request, Response } from 'express';
import butter from '../lib/buttercms';
const router = Router();
interface PageFields {
headline: string;
subheadline: string;
hero_image: string;
body: string;
}
router.get('/', (req: Request, res: Response) => renderPage('home', res));
router.get('/:slug', (req: Request<{ slug: string }>, res: Response) => renderPage(req.params.slug, res));
async function renderPage(slug: string, res: Response) {
try {
const response = await butter.page.retrieve<PageFields>('landing_page', slug);
if (!response.data?.data) {
throw new Error('Failed to load page from ButterCMS');
}
res.render('landing', { page: response.data.data });
} catch (err) {
res.status(404).render('404');
}
}
export default router;
<!-- views/landing.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= page.fields.headline %></title>
</head>
<body>
<main>
<h1><%= page.fields.headline %></h1>
<p><%= page.fields.subheadline %></p>
<% if (page.fields.hero_image) { %>
<img src="<%= page.fields.hero_image %>" alt="<%= page.fields.headline %>" />
<% } %>
<%- page.fields.body %>
</main>
</body>
</html>
Collections
- JavaScript
- TypeScript
// routes/brands.js
const express = require('express');
const router = express.Router();
const butter = require('../lib/buttercms');
router.get('/', async (req, res) => {
const response = await butter.content.retrieve(['brands']);
if (!response.data?.data) {
throw new Error('Failed to load brands from ButterCMS');
}
res.render('brands', { brands: response.data.data.brands });
});
module.exports = router;
// routes/brands.ts
import { Router, Request, Response } from 'express';
import butter from '../lib/buttercms';
const router = Router();
interface Brand {
name: string;
logo: string;
description: string;
}
router.get('/', async (req: Request, res: Response) => {
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;
res.render('brands', { brands });
});
export default router;
<!-- views/brands.ejs -->
<main>
<h1>Our Brands</h1>
<ul>
<% brands.forEach(brand => { %>
<li>
<img src="<%= brand.logo %>" alt="<%= brand.name %>" />
<h2><%= brand.name %></h2>
<%- brand.description %>
</li>
<% }); %>
</ul>
</main>
Dynamic components
Component Renderer
- JavaScript
- TypeScript
// lib/component-renderer.js
const componentTemplates = {
hero: 'components/hero',
features: 'components/features',
testimonials: 'components/testimonials',
cta: 'components/cta',
};
function getComponentTemplate(type) {
return componentTemplates[type] || null;
}
module.exports = { getComponentTemplate };
// lib/component-renderer.ts
export interface ButterComponent {
type: string;
fields: Record<string, unknown>;
}
const componentTemplates: Record<string, string> = {
hero: 'components/hero',
features: 'components/features',
testimonials: 'components/testimonials',
cta: 'components/cta',
};
export function getComponentTemplate(type: string): string | null {
return componentTemplates[type] || null;
}
Component template
<!-- views/components/hero.ejs -->
<section class="hero">
<h1><%= fields.headline %></h1>
<p><%= fields.subheadline %></p>
<% if (fields.button_label) { %>
<a href="<%= fields.button_url %>" class="btn"><%= fields.button_label %></a>
<% } %>
<% if (fields.image) { %>
<img src="<%= fields.image %>" alt="<%= fields.headline %>" />
<% } %>
</section>
Using in routes
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.- JavaScript
- TypeScript
// routes/component-pages.js
const express = require('express');
const router = express.Router();
const butter = require('../lib/buttercms');
const { getComponentTemplate } = require('../lib/component-renderer');
router.get('/:slug', async (req, res) => {
try {
const response = await butter.page.retrieve('component_page', req.params.slug);
if (!response.data?.data) {
throw new Error('Failed to load page from ButterCMS');
}
const page = response.data.data;
// Map components to their templates
const components = page.fields.body.map(component => ({
template: getComponentTemplate(component.type),
fields: component.fields,
})).filter(c => c.template);
res.render('component-page', { page, components });
} catch (err) {
res.status(404).render('404');
}
});
module.exports = router;
// routes/component-pages.ts
import { Router, Request, Response } from 'express';
import butter from '../lib/buttercms';
import { getComponentTemplate, ButterComponent } from '../lib/component-renderer';
const router = Router();
interface ComponentPageFields {
body: ButterComponent[];
}
router.get('/:slug', async (req: Request<{ slug: string }>, res: Response) => {
try {
const response = await butter.page.retrieve<ComponentPageFields>('component_page', req.params.slug);
if (!response.data?.data) {
throw new Error('Failed to load page from ButterCMS');
}
const page = response.data.data;
const components = page.fields.body
.map((component: ButterComponent) => ({
template: getComponentTemplate(component.type),
fields: component.fields,
}))
.filter((mapped): mapped is { template: string; fields: ButterComponent['fields'] } => mapped.template !== null);
res.render('component-page', { page, components });
} catch (err) {
res.status(404).render('404');
}
});
export default router;
<!-- views/component-page.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= page.fields.seo?.title || 'Page' %></title>
</head>
<body>
<main>
<% components.forEach(component => { %>
<%- include(component.template, { fields: component.fields }) %>
<% }); %>
</main>
</body>
</html>
Blog
- Blog Post List
- Single Blog Post
- JavaScript
- TypeScript
// routes/blog.js
const express = require('express');
const router = express.Router();
const butter = require('../lib/buttercms');
router.get('/', async (req, res) => {
const page = Number(req.query.page) || 1;
const response = await butter.post.list({ page, page_size: 10 });
if (!response.data?.data) {
throw new Error('Failed to load blog posts from ButterCMS');
}
res.render('blog/list', {
posts: response.data.data,
meta: response.data.meta,
});
});
module.exports = router;
// routes/blog.ts
import { Router, Request, Response } from 'express';
import butter from '../lib/buttercms';
const router = Router();
router.get('/', async (req: Request, res: Response) => {
const page = Number(req.query.page) || 1;
const response = await butter.post.list({ page, page_size: 10 });
if (!response.data?.data) {
throw new Error('Failed to load blog posts from ButterCMS');
}
res.render('blog/list', {
posts: response.data.data,
meta: response.data.meta,
});
});
export default router;
<!-- views/blog/list.ejs -->
<main>
<h1>Blog</h1>
<ul>
<% posts.forEach(post => { %>
<li>
<h2><a href="https://buttercms.com/blog/<%= post.slug %>"><%= post.title %></a></h2>
<%- post.summary %>
<span>By <%= post.author.first_name %> <%= post.author.last_name %></span>
</li>
<% }); %>
</ul>
<% if (meta.next_page) { %>
<a href="/blog?page=<%= meta.next_page %>">Next Page</a>
<% } %>
</main>
- JavaScript
- TypeScript
// routes/blog.js (continued)
router.get('/:slug', async (req, res) => {
try {
const response = await butter.post.retrieve(req.params.slug);
if (!response.data?.data) {
throw new Error('Failed to load blog post from ButterCMS');
}
res.render('blog/post', { post: response.data.data });
} catch (err) {
res.status(404).render('404');
}
});
// routes/blog.ts (continued)
router.get('/:slug', async (req: Request<{ slug: string }>, res: Response) => {
try {
const response = await butter.post.retrieve(req.params.slug);
if (!response.data?.data) {
throw new Error('Failed to load blog post from ButterCMS');
}
res.render('blog/post', { post: response.data.data });
} catch (err) {
res.status(404).render('404');
}
});
<!-- views/blog/post.ejs -->
<article>
<h1><%= post.title %></h1>
<p>
By <%= post.author.first_name %> <%= post.author.last_name %>
<% if (post.published) { %>
on <%= new Date(post.published).toLocaleDateString() %>
<% } %>
</p>
<% if (post.featured_image) { %>
<img src="<%= post.featured_image %>" alt="<%= post.title %>" />
<% } %>
<%- post.body || '' %>
<a href="/blog">Back to Posts</a>
</article>
Caching
Use a caching middleware for better performance:// middleware/cache.js
const cache = new Map();
const CACHE_TTL = 3600000; // 1 hour
function butterCache(keyPrefix) {
return async (req, res, next) => {
const cacheKey = `${keyPrefix}_${req.originalUrl}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
res.locals.butterCache = cached.data;
return next();
}
// Store original render function
const originalRender = res.render.bind(res);
// Override render to cache the data
res.render = (view, options) => {
cache.set(cacheKey, {
data: options,
timestamp: Date.now(),
});
originalRender(view, options);
};
next();
};
}
function invalidateCache(pattern) {
for (const key of cache.keys()) {
if (key.includes(pattern)) {
cache.delete(key);
}
}
}
module.exports = { butterCache, invalidateCache };
Webhook handler
// routes/webhooks.js
const express = require('express');
const router = express.Router();
const { invalidateCache } = require('../middleware/cache');
router.post('/buttercms', express.json(), (req, res) => {
const { webhook_type } = req.body;
if (webhook_type?.includes('published') ||
webhook_type?.includes('updated') ||
webhook_type?.includes('deleted')) {
invalidateCache('butter');
}
res.json({ status: 'ok' });
});
module.exports = router;
SEO
// middleware/seo.js
function extractSeoData(page) {
const seo = page.fields.seo || {};
return {
title: seo.title || page.fields.headline || 'Page',
description: seo.description || '',
ogTitle: seo.og_title || seo.title || page.fields.headline,
ogDescription: seo.og_description || seo.description,
ogImage: seo.og_image || page.fields.hero_image || '',
};
}
module.exports = { extractSeoData };
<!-- views/partials/head.ejs -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= seo.title %></title>
<meta name="description" content="<%= seo.description %>">
<!-- Open Graph -->
<meta property="og:title" content="<%= seo.ogTitle %>">
<meta property="og:description" content="<%= seo.ogDescription %>">
<% if (seo.ogImage) { %>
<meta property="og:image" content="<%= seo.ogImage %>">
<% } %>
</head>
Complete app setup
// app.js
require('dotenv').config();
const express = require('express');
const pagesRouter = require('./routes/pages');
const blogRouter = require('./routes/blog');
const brandsRouter = require('./routes/brands');
const webhooksRouter = require('./routes/webhooks');
const app = express();
app.set('view engine', 'ejs');
app.use('/blog', blogRouter);
app.use('/brands', brandsRouter);
app.use('/webhooks', webhooksRouter);
app.use('/', pagesRouter);
// 404 handler
app.use((req, res) => {
res.status(404).render('404');
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).render('500');
});
app.listen(process.env.PORT || 3000);
Resources
Express.js Starter
Pre-configured starter project
JavaScript SDK
Complete SDK reference
GitHub Repository
View source code
Content API
REST API documentation