Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

Contentful and Next.js: Content Model, Delivery API, and ISR That Revalidates

Sean

Platform Writer

Sep 22, 2026
8 min read

Connecting Contentful to Next.js means defining a content model of content types and fields in Contentful, fetching that content in a server component using the Content Delivery API key, and choosing how fresh the rendered page should be: static at build time, revalidated on an interval with ISR, revalidated on demand from a webhook when an editor publishes, or fetched live in preview mode for unpublished drafts. The part that catches people out is not the fetching: it is that ISR and preview mode both require Next.js running on a Node.js server, not the static export output mode, which removes the ability to revalidate anything after the build.

Contentful and Next.js: Content Model, Delivery API, and ISR That Revalidates

A Contentful and Next.js integration usually starts from a starter template, which gets the fetching working quickly and skips past the two decisions that matter for anything beyond a demo: how content gets fresh after publishing, and what hosting mode that freshness depends on.

This post covers the content model, the fetch, ISR, preview, and rich text, then the hosting requirement underneath all of it.

Table of contents

The content model

A content type in Contentful is a schema: a Blog Post type might have fields for title (short text), slug (short text, used as the URL segment), body (rich text), and coverImage (a media reference). Fields map roughly to columns in a database table, and Contentful’s web app is where the schema gets defined, not code.

Getting the model right before writing content matters more than in a traditional CMS, because changing a field’s type later, short text to rich text for example, is a migration, not an edit.

A reference field is where this diverges most from a database schema. Contentful lets a field reference another entry directly, an Author linked from a Blog Post for example, and the Delivery API can resolve a limited depth of those links in a single request using an include parameter, which avoids a second round trip for a field that is used on nearly every page.

The Delivery API key

Contentful’s Content Delivery API serves only published content, and reading from it needs a Space ID and a Content Delivery API access token, both created under Settings, API keys in the Contentful web app.

CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_ACCESS_TOKEN=your-delivery-token
CONTENTFUL_ENVIRONMENT=master

Keep these as environment variables, not in code, and note that the Delivery token is different from the Preview token covered further down; using the wrong one is the most common reason unpublished content either shows up unexpectedly or does not show up at all.

A token scoped to the wrong environment is a common source of confusion once a project has more than master. Contentful’s environments, used for staging content changes before they reach production, each need their own value for CONTENTFUL_ENVIRONMENT, and pointing the Delivery token at one environment while the code requests another returns an empty result set rather than an error, which looks like a fetching bug.

Fetching in server components

In the App Router, a server component can call the Contentful client directly, no separate API route needed, since server components run on the server and can hold the access token safely.

import { createClient } from 'contentful';

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const entries = await client.getEntries({
    content_type: 'blogPost',
    'fields.slug': params.slug,
    limit: 1,
  });
  const post = entries.items[0];
  return <article><h1>{post.fields.title as string}</h1></article>;
}

A request for a slug with no matching entry returns an empty items array rather than throwing, so the component needs an explicit check before reading entries.items[0], or a missing page renders as a crash from reading a field off undefined instead of a clean 404. Next.js also caches this fetch by default the same way it caches any fetch call, which interacts directly with the revalidate setting covered next.

ISR and revalidation

Fetching with a revalidate option tells Next.js to serve a cached version of the page and regenerate it in the background once the interval passes, rather than fetching fresh on every request.

export const revalidate = 60; // seconds

That is time-based ISR, useful as a baseline, but it means a published change can take up to a minute to show up. On-demand revalidation removes the wait: a route handler calling revalidatePath or revalidateTag, hit by a webhook from Contentful the moment content is published, updates the page immediately instead of waiting for the interval.

A webhook route handler that does not verify its own secret is a real risk once the URL is known. Anyone who finds it can trigger a rebuild storm by hitting it repeatedly, which is why the handler should check a shared secret, sent as a header or a query parameter, before calling revalidatePath, and return a 401 for anything that does not match.

Preview mode

Contentful’s Preview API, using a separate Preview access token, returns draft and unpublished content alongside published content, and Next.js’s Draft Mode is the mechanism that opts a request into seeing it.

import { draftMode } from 'next/headers';

export async function GET(request: Request) {
  (await draftMode()).enable();
  return Response.redirect(new URL('/preview-target', request.url));
}

Contentful’s preview URL, configured under Settings, Content preview, points editors at this route, which enables Draft Mode and redirects to the actual page, now rendering with the preview token and showing unpublished changes.

Exiting preview needs an explicit route too, calling draftMode().disable() and redirecting back to the normal page. Draft Mode persists in a cookie until it is turned off, and an editor left in it after finishing a review will keep seeing unpublished content on every page, not just the one they were checking, until that cookie is cleared. That cookie is not tied to Contentful’s own login, so a shared preview link left open in a browser is effectively a standing bypass of what is published, worth remembering before sharing a preview URL outside the team.

Rendering rich text

Contentful’s rich text field returns a structured document, not HTML, and @contentful/rich-text-react-renderer converts that document into React elements, with a renderNode map for anything the defaults do not cover, most commonly embedded assets and entries.

import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS } from '@contentful/rich-text-types';

const options = {
  renderNode: {
    [BLOCKS.EMBEDDED_ASSET]: (node: any) => (
      <img src={node.data.target.fields.file.url} alt={node.data.target.fields.title} />
    ),
  },
};

documentToReactComponents(post.fields.body, options);

Embedded entries, not just assets, need their own case in renderNode under BLOCKS.EMBEDDED_ENTRY, and the shape of node.data.target depends entirely on which content type was embedded, so a rich text field that allows multiple entry types linked inside it needs a switch on the entry’s content type sys.id before deciding how to render each one. Without a case for a given embed type, the renderer silently drops it rather than throwing, which means a missing renderNode entry shows up as content that looks fine in Contentful’s editor and quietly vanishes on the page.

The hosting side: ISR needs a Node runtime

output: ‘export’ in next.config produces static HTML with no server, which disables ISR, revalidateTag, revalidatePath, Draft Mode, and route handlers that do anything beyond return a static response. Any of the revalidation or preview behavior above needs Next.js actually running as a server, not exported as files.

The webhook that drives on-demand revalidation also needs a stable server URL to call and an environment variable for its own secret, checked in the route handler so an arbitrary request cannot trigger revalidation. Preview and production typically use different Contentful spaces or environments, which means different environment variable values per deploy environment, not one shared set.

A practical setup keeps two full sets of these variables, one for a preview deployment and one for production, rather than one set with a runtime branch in code deciding which space to read from. A preview deploy pointed at the wrong Contentful space by a typo in one variable is a much easier mistake to make than to notice, since both environments look identical until the content itself differs.

How this fits the rest of the stack

Running Next.js as a server, rather than exporting it as static files, is what makes ISR and preview mode work at all with Contentful. RunxBuild runs Next.js as a Node service from the repository, with environment variables set per environment for exactly this kind of per-space Contentful setup, and the RunxBuild hosting calculator shows what that costs alongside a database if the app needs one beyond Contentful itself.

Useful related references:

FAQ

How do I connect Contentful to Next.js?

Create a Space ID and a Content Delivery API access token in Contentful’s settings, set them as environment variables, and call Contentful’s client from a server component using the App Router, or from getStaticProps or getServerSideProps in the Pages Router. The client fetches entries by content type and any field filters needed, such as a slug.

Does ISR work with Contentful?

Yes. Set a revalidate value on the fetch or the route segment for time-based regeneration, or trigger revalidatePath or revalidateTag from a route handler called by a Contentful webhook for on-demand updates the moment content publishes. Both require Next.js running on a Node server rather than exported as static files.

Can I use Contentful with Next.js static export?

Only for content that never needs to update after the build, since static export disables ISR, on-demand revalidation, Draft Mode, and any route handler beyond a fixed response. Most Contentful integrations want at least one of those, which means running Next.js as a server rather than using output: export.

How does preview mode work with Contentful and Next.js?

Contentful’s Preview API, using a separate Preview access token, returns unpublished and draft content. Next.js’s Draft Mode, enabled from a route handler that Contentful’s preview URL points at, opts the request into fetching with that preview token, so an editor sees draft changes on the live page template before publishing.

#contentful nextjs#headless cms#next.js isr#content delivery api#rich text rendering