Skip to content

Headless CMS + SSG — Content Management for Static Sites

DodaTech Updated 2026-06-28 5 min read

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

  1. Hardcoding CMS API credentials. Always use environment variables for API tokens. Leaked credentials in source code are a security risk.
  2. Not handling CMS API rate limits. Large content sets need pagination and concurrent request limits. Batch requests to avoid 429 errors.
  3. Missing error handling for CMS failures. If the CMS is down, the build fails. Cache API responses locally as fallback.
  4. Ignoring content preview workflows. Editors need to preview content before publishing. Implement preview mode in your SSG.
  5. 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

  1. How does a headless CMS connect to an SSG?
  2. What is the difference between Contentful, Sanity, and Strapi?
  3. How do you implement content previews with Next.js preview mode?
  4. What is the role of Webhooks in CMS + SSG integration?
  5. 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

Do I need a headless CMS for an SSG site?

No. Markdown files work great for developer-maintained content. Use a headless CMS when non-technical team members need to create or edit content.

Which headless CMS is best for static sites?

Contentful is most popular, Sanity offers real-time collaboration, Strapi is open-source and self-hosted. Choose based on budget, features, and team preferences.

How do CMS webhooks trigger SSG rebuilds?

The CMS sends a POST request to your build hook URL when content changes. Netlify, Vercel, and other platforms provide build hook URLs for this purpose.

Can I use multiple CMS sources in one SSG site?

Yes. Fetch from different CMS APIs in getStaticProps or create separate source plugins that merge into a unified data layer.

How do I handle media assets from CMS in SSG?

Most CMS platforms have image CDNs. Reference remote URLs or download assets during build. Use responsive image techniques (srcset, sizes) for optimization.

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