NetcaneTechnologies

Contentful

Contentful + Next.js: Production Integration Blueprint

A repeatable pattern for fetching Contentful entries in Next.js with typed models, draft preview, and ISR-friendly caching.

Yasir Haleem4 min read

Contentful pairs well with Next.js when you treat the Delivery API as a read-only content layer and keep all fetch logic in a single module. This blueprint covers the client setup, typed entry models, linked entries, preview for drafts, and cache tags so your marketing site stays fast without stale content.

Content model first

Before writing fetch code, align on entry types: page, blogPost, author, and reusable section entries for block-based pages. Use references for relations (author → post, page → sections) instead of duplicating fields. In Contentful, enable localization only where you need it—every locale adds query complexity on the front end.

Name fields consistently (slug, title, seoTitle, seoDescription) so your Next.js generateMetadata can map them without CMS-specific branching in page components.

Typed client wrapper

Use the official contentful SDK on the server only. Centralize space ID, environment, and tokens in env vars. Never expose the preview token to the client.

// lib/contentful.ts
import { createClient, type Entry, type EntrySkeletonType } from "contentful";
 
const delivery = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
  environment: process.env.CONTENTFUL_ENVIRONMENT ?? "master",
});
 
const preview = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
  host: "preview.contentful.com",
  environment: process.env.CONTENTFUL_ENVIRONMENT ?? "master",
});
 
export function getContentfulClient(previewMode = false) {
  return previewMode ? preview : delivery;
}

Keep this file free of React imports. Pages and route handlers import getContentfulClient and pass draftMode().isEnabled when needed.

Entry types and mappers

SDK responses are verbose. Define skeleton types for each content type, then map to a normalized shape your components consume.

// types/content.ts
export type BlogPost = {
  slug: string;
  title: string;
  excerpt: string;
  body: string;
  publishedAt: string;
  author?: { name: string; avatarUrl?: string };
  cover?: { url: string; alt: string; width: number; height: number };
};
 
// lib/contentful-mappers.ts
import type { Asset, Entry } from "contentful";
 
type BlogPostFields = {
  slug: string;
  title: string;
  excerpt: string;
  body: string;
  publishedDate: string;
  author?: Entry<{ name: string; avatar?: Asset }>;
  coverImage?: Asset;
};
 
export function mapBlogPost(entry: Entry<BlogPostFields>): BlogPost {
  const cover = entry.fields.coverImage;
  return {
    slug: entry.fields.slug,
    title: entry.fields.title,
    excerpt: entry.fields.excerpt,
    body: entry.fields.body,
    publishedAt: entry.fields.publishedDate,
    author: entry.fields.author
      ? {
          name: entry.fields.author.fields.name,
          avatarUrl: entry.fields.author.fields.avatar?.fields.file?.url
            ? `https:${entry.fields.author.fields.avatar.fields.file.url}`
            : undefined,
        }
      : undefined,
    cover: cover?.fields.file?.url
      ? {
          url: `https:${cover.fields.file.url}`,
          alt: cover.fields.description ?? entry.fields.title,
          width: cover.fields.file.details.image?.width ?? 1200,
          height: cover.fields.file.details.image?.height ?? 630,
        }
      : undefined,
  };
}

Mappers are where you handle https: prefixes on asset URLs and optional relations—so UI components stay dumb.

Fetching with include depth

Linked entries are not returned unless you set include. For a blog post with author and cover, depth 2 is usually enough.

// lib/contentful-posts.ts
import { getContentfulClient } from "./contentful";
import { mapBlogPost } from "./contentful-mappers";
 
export async function getPostBySlug(slug: string, preview = false) {
  const client = getContentfulClient(preview);
  const res = await client.getEntries({
    content_type: "blogPost",
    "fields.slug": slug,
    include: 2,
    limit: 1,
  });
  const entry = res.items[0];
  if (!entry) return null;
  return mapBlogPost(entry);
}

For list pages, request only the fields you need (select) and paginate with skip / limit or cursor-based fetching for large archives.

Draft preview in App Router

Contentful preview uses the Preview API and preview.contentful.com. Wire it to Next.js draft mode:

// app/api/draft/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get("secret");
  const slug = searchParams.get("slug");
 
  if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET || !slug) {
    return new Response("Invalid token", { status: 401 });
  }
 
  (await draftMode()).enable();
  redirect(`/blog/${slug}`);
}

In getPostBySlug, pass preview: true when draft mode is enabled. Contentful’s preview URL can point to this route with secret and slug query params.

Caching and webhooks

Use fetch with next: { tags: ['contentful'] } if you wrap REST calls, or revalidate by tag when Contentful sends a publish webhook to /api/revalidate. Tag per content type (post:${slug}, page:${slug}) for surgical invalidation instead of purging the whole site.

Production checklist: Delivery token in production only; preview token server-only; CORS not needed for server fetches; validate webhook signatures; map assets through next/image with Contentful host in remotePatterns.

Summary

Contentful + Next.js stays maintainable with a single client module, typed mappers, explicit include depth, and draft mode for editors. Keep components on normalized types, revalidate on publish, and your team gets a fast front end without fighting the CMS response shape on every page.

About the author

Yasir Haleem is founder and lead engineer at Netcane Technologies. He builds production Next.js sites with headless CMS platforms — Strapi, Contentful, Sanity, and WordPress — with a focus on performance, SEO, and maintainable architecture.

Let's work together

Tell us about your project. We respond within one business day with a clear scope, timeline, and estimate.