How to implement Contentful preview mode in Gatsby
Gatsby’s static generation model conflicts with real-time headless CMS (Content Management System) drafting. Unpublished content requires isolated data sourcing and explicit routing overrides. This guide resolves draft routing failures, token validation errors, and cache conflicts. We configure dual GraphQL endpoints, secure preview credentials, and automate cache invalidation.
Core Architecture: Dual GraphQL Sources & Draft Routing
Contentful separates delivery and preview APIs. The delivery API serves published content. The preview API exposes drafts and unpublished revisions. Gatsby fetches data at build time, requiring explicit plugin configuration to isolate draft nodes. Establishing a robust Preview & Draft Workflow Implementation ensures draft entries inject into the GraphQL layer without corrupting production builds.
Configure gatsby-source-contentful with separate credentials. Point the host to the preview domain.
// gatsby-config.ts
import type { GatsbyConfig } from 'gatsby';
const config: GatsbyConfig = {
plugins: [
{
resolve: 'gatsby-source-contentful',
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID!,
// CRITICAL: Route to preview host for draft access
host: 'preview.contentful.com',
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
},
},
],
};
export default config;
DX (Developer Experience) Tradeoff: Preview builds fetch unoptimized assets and draft metadata. Expect longer build times and larger GraphQL payloads compared to production deployments.
Token Authentication & Environment Isolation
Preview tokens grant read access to unpublished revisions. Never expose them in client-side bundles or production environments. Inject credentials via .env.development and validate during gatsby develop and gatsby build. While dynamic frameworks handle this via runtime API routes, Gatsby requires build-time environment injection. Reference Setting Up Live Preview in Next.js for cross-framework token validation patterns.
Implement strict environment guards to prevent production leakage.
// lib/env-validation.ts
export function validatePreviewEnv() {
const token = process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN;
const isProduction = process.env.NODE_ENV === 'production';
// CRITICAL: Abort if preview token leaks into production
if (isProduction && token) {
throw new Error('Preview token detected in production build. Abort.');
}
if (!token && !isProduction) {
throw new Error('Missing CONTENTFUL_PREVIEW_ACCESS_TOKEN in development.');
}
}
Call this validation in gatsby-config.ts before plugin initialization. Failing fast prevents silent fallbacks to published content.
Reproducible Scenario: Draft Pages Return 404 on Localhost
Unpublished entries often fail to generate pages during local development. Gatsby skips page creation when sys.publishedAt filters conflict with gatsby-node.js logic. Stale .cache directories and missing GraphQL fragment selections typically cause this behavior.
Verify your GraphQL query explicitly requests draft metadata.
# src/templates/draft-page.ts
query DraftPageQuery($slug: String!) {
contentfulPage(slug: { eq: $slug }) {
id
slug
title
body { raw }
sys {
# CRITICAL: Null indicates an unpublished draft
publishedAt
status
}
}
}
If sys.publishedAt returns a timestamp, the node is published. If it returns null, Gatsby treats it as a draft. Ensure your createPage logic handles null values correctly.
Step-by-Step Resolution: Fixing Preview Routing & Cache Conflicts
Follow these steps to restore draft visibility and isolate preview routes. Purge stale build artifacts using gatsby clean. Update gatsby-node.js to accept a draft boolean from Contentful. Implement onCreatePage to override static paths for preview URLs. Validate webhook payloads to trigger incremental builds. Add client-side hydration checks to prevent FOUC (Flash of Unstyled Content) during draft transitions.
Route draft pages to /preview/:slug using gatsby-plugin-create-client-paths.
// gatsby-node.ts
import type { GatsbyNode } from 'gatsby';
export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql<{ allContentfulPage: { nodes: Array<{ slug: string; sys: { publishedAt: string | null }> }> }>(`
{ allContentfulPage { nodes { slug sys { publishedAt } } } }
`);
result?.data?.allContentfulPage.nodes.forEach((node) => {
const isDraft = node.sys.publishedAt === null;
// CRITICAL: Branch routing logic based on publication state
createPage({
path: isDraft ? `/preview/${node.slug}` : `/${node.slug}`,
component: require.resolve('./src/templates/page-template.tsx'),
context: { slug: node.slug, isDraft },
});
});
};
DX Tradeoff: Client-side routing for /preview/* bypasses Gatsby’s static HTML generation. You lose initial SSR (Server-Side Rendering) for drafts, but gain instant preview updates without full rebuilds.
Prevention & Edge Case Handling
Prevent stale previews by implementing Contentful webhook retries and Gatsby incremental builds. Validate draft tokens on every build cycle. Monitor GraphQL schema drift during content model updates.
Configure a webhook handler to target Gatsby’s __refresh endpoint and implement cache busting.
// api/webhook.ts
import { IncomingMessage, ServerResponse } from 'http';
export default async function handler(req: IncomingMessage, res: ServerResponse) {
if (req.method !== 'POST') return res.writeHead(405).end();
const secret = process.env.CONTENTFUL_WEBHOOK_SECRET;
const payload = req.headers['x-contentful-signature'];
// CRITICAL: Validate signature before triggering builds
if (process.env.NODE_ENV === 'production' && payload !== secret) {
return res.writeHead(401).end('Invalid webhook signature');
}
await fetch(`${process.env.GATSBY_SITE_URL}/__refresh`, { method: 'POST' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'preview_refresh_queued' }));
}
Append ?preview=true to draft URLs to bypass CDN (Content Delivery Network) caching. This forces browsers to request fresh HTML during active editing sessions.
Pitfalls
- Cache Persistence: Gatsby aggressively caches GraphQL results. Run
gatsby cleanbefore switching between preview and production environments. - Schema Drift: Adding required fields to Contentful breaks existing GraphQL queries. Use optional fields or provide default values in resolvers.
- Token Rotation: Expired preview tokens cause silent build failures. Automate rotation alerts via CI/CD (Continuous Integration/Continuous Deployment) pipelines.
- FOUC on Drafts: Client-side hydration delays can cause layout shifts. Implement React Suspense boundaries around draft components.
FAQ
Q: Why do draft pages return 404 after gatsby develop?
A: The .cache directory likely contains stale published nodes. Run gatsby clean and restart the dev server to force a fresh preview fetch.
Q: Can I use the same Contentful space for preview and production?
A: Yes, but isolate credentials using environment variables. Never hardcode tokens. Use separate .env files for development and CI/CD.
Q: How do I handle image assets in preview mode?
A: Contentful’s preview API returns draft image URLs. Configure gatsby-plugin-image to accept the preview host. Ensure your CDN allows cross-origin requests for draft assets.
Q: Does Gatsby support real-time live preview like Next.js? A: Gatsby is statically generated. You can approximate live preview using client-side routing and WebSocket polling, but it requires custom hydration logic and sacrifices static performance.