Headless CMS + SSG — Content Management for Static Sites
In this tutorial, you will learn about Headless CMS + SSG. We cover key concepts, practical examples, and best practices to help you master this topic.
Combining a headless CMS with SSG lets content teams manage content in a friendly editor while developers build fast static sites.
What You'll Learn
By the end of this tutorial, you'll understand how headless CMS platforms work with SSG, how to integrate Contentful, Sanity, and Strapi, how to handle content previews, and how to trigger rebuilds on content changes.
Why It Matters
Markdown files are developer-friendly but not content-team-friendly. A headless CMS provides a visual editor, asset management, and publishing workflows while SSG handles the rendering and performance.
Real-World Use
A marketing team of 5 writers manages blog content in Contentful. They write, edit, and publish without touching code. When they publish, a Webhook triggers a rebuild, and the static site updates within minutes.
CMS + SSG Workflow
graph LR
A[Content Writers] --> B[Headless CMS
Contentful / Sanity / Strapi]
B --> C[CMS API
REST / GraphQL]
C --> D[SSG Build Process]
D --> E[Fetch content
at build time]
E --> F[Transform &
render pages]
F --> G[Static HTML output]
G --> H[CDN Deploy]
H --> I[Users see
updated content]
B -.-> J[Webhook trigger]
J -.-> D
style B fill:#4a90d9,color:#fff
style D fill:#e67e22,color:#fff
style G fill:#27ae60,color:#fff
Contentful Integration
// Next.js + Contentful — getStaticProps
const contentful = require('contentful');
const client = contentful.createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});
// Fetch all blog posts
export async function getStaticProps() {
const entries = await client.getEntries({
content_type: 'blogPost',
order: '-fields.publishDate',
limit: 20,
});
const posts = entries.items.map(item => ({
id: item.sys.id,
title: item.fields.title,
slug: item.fields.slug,
excerpt: item.fields.excerpt,
content: item.fields.content,
image: item.fields.featuredImage?.fields?.file?.url,
publishDate: item.fields.publishDate,
author: item.fields.author?.fields?.name,
}));
return {
props: { posts },
revalidate: 60,
};
}
Sanity Integration
// Next.js + Sanity — GROQ queries
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DATASET || 'production',
apiVersion: '2024-01-01',
useCdn: true,
});
const builder = imageUrlBuilder(client);
export async function getStaticProps() {
const query = `*[_type == "post" && defined(slug.current)] |
order(publishedAt desc) [0...20] {
_id,
title,
"slug": slug.current,
excerpt,
body,
"imageUrl": mainImage.asset->url,
publishedAt,
"authorName": author->name
}`;
const posts = await client.fetch(query);
// Transform image URLs with Sanity's image builder
const postsWithImages = posts.map(post => ({
...post,
imageUrl: post.imageUrl
? builder.image(post.imageUrl).width(800).url()
: null
}));
return {
props: { posts: postsWithImages },
revalidate: 60,
};
}
Strapi Integration
// Next.js + Strapi (self-hosted CMS)
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
export async function getStaticProps() {
const res = await fetch(`${STRAPI_URL}/api/posts`, {
headers: {
Authorization: `Bearer ${process.env.STRAPI_TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`Strapi fetch failed: ${res.status}`);
}
const { data } = await res.json();
const posts = data.map(item => ({
id: item.id,
title: item.attributes.title,
slug: item.attributes.slug,
content: item.attributes.content,
image: item.attributes.image?.data?.attributes?.url,
publishedAt: item.attributes.publishedAt,
}));
return {
props: { posts },
revalidate: 60,
};
}
// Webhook endpoint for Strapi
export async function handleStrapiWebhook(req, res) {
if (req.body.event === 'entry.publish') {
// Trigger on-demand revalidation
await res.revalidate('/blog');
await res.revalidate(`/blog/${req.body.entry.slug}`);
}
res.json({ received: true });
}
Content Previews
// pages/api/preview.js — Next.js preview mode
export default async function handler(req, res) {
const { slug, secret } = req.query;
// Verify preview secret
if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
// Enable preview mode
res.setPreviewData({
slug,
timestamp: Date.now(),
});
// Redirect to the preview page
res.redirect(`/blog/${slug}`);
}
// pages/blog/[slug].js — Handle preview
export async function getStaticProps({ params, preview = false }) {
// Use preview API when in preview mode
const host = preview
? 'preview.contentful.com'
: 'cdn.contentful.com';
const client = contentful.createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: preview
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN,
host,
});
const entry = await client.getEntries({
content_type: 'blogPost',
'fields.slug': params.slug,
});
if (!entry.items.length) {
return { notFound: true };
}
return {
props: { post: entry.items[0].fields },
revalidate: preview ? 1 : 60,
};
}
Common Mistakes
- Hardcoding CMS API credentials. Always use environment variables for API tokens. Leaked credentials in source code are a security risk.
- Not handling CMS API rate limits. Large content sets need pagination and concurrent request limits. Batch requests to avoid 429 errors.
- Missing error handling for CMS failures. If the CMS is down, the build fails. Cache API responses locally as fallback.
- Ignoring content preview workflows. Editors need to preview content before publishing. Implement preview mode in your SSG.
- Not optimizing CMS images. CMS platforms often serve unoptimized images. Use image transformation APIs (Contentful Images API, Sanity image pipeline) for responsive images.
Practice Questions
- How does a headless CMS connect to an SSG?
- What is the difference between Contentful, Sanity, and Strapi?
- How do you implement content previews with Next.js preview mode?
- What is the role of Webhooks in CMS + SSG integration?
- How do you handle CMS image optimization during the SSG build?
Challenge: Set up a complete CMS + SSG pipeline: create content in Contentful (or sanity.io sandbox), fetch it in Next.js getStaticProps, implement preview mode, configure a webhook for on-demand revalidation, and deploy.
FAQ
Mini Project
Build a blog with CMS integration: set up a Contentful space with a blog post content type, create 5 posts, integrate with Next.js SSG using getStaticProps, implement preview mode, configure a webhook for automatic rebuilds, and deploy to Vercel.
What's Next
Your CMS-integrated site is ready. Now add SSG Search functionality to help users find content across your static site.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro