How to add a CMS to a Next.js site without losing speed
Pick a headless CMS, fetch content in server components with cache tags, refresh pages with a publish webhook, and add draft previews. With working code.
By Sohail Hussain15 November 20265 min read
To add a CMS to a Next.js site, connect a headless CMS through its API, fetch the content inside server components wrapped in a cached function with a cache tag, and have the CMS call a webhook on publish that revalidates that tag. Pages stay pre-rendered and fast for visitors, and edits go live within seconds of an editor pressing publish. Draft Mode then gives editors a preview of unpublished changes.
The steps below use the App Router in Next.js 16, which is current as of October 2026. The code is CMS-agnostic: swap the fetch URL for your CMS's API or SDK.
Step 1: Do you need a CMS at all?
A CMS is worth it when people who do not write code need to change content without waiting for a developer. That covers most company sites with a blog, news, case studies, team pages or job listings.
It is not worth it when only developers touch the content, or when the content changes a few times a year. Markdown or MDX files in the code repository are free, versioned with the code, and need no extra service. This blog works that way: every post is an MDX file in the repository, reviewed and published through the same process as code. For a team of writers who do not use Git, that would be the wrong choice.
If you are planning a new site and are unsure, our website design and development projects start with this question, because it changes the build more than any other.
Step 2: Which headless CMS should you choose?
A headless CMS stores and edits content and exposes it through an API. It has no say in how the site looks. The main options we see with Next.js:
| CMS | Where it runs | Editing experience | Good fit |
|---|---|---|---|
| Sanity | Hosted content store; the editor can be hosted inside your Next.js site | Customisable, real-time collaboration | Structured content, custom workflows |
| Payload | Open source; installs inside the Next.js app with your own database | Admin panel generated from your config | Teams that want to own the whole stack |
| Contentful | Fully hosted | Polished, form-based | Larger teams, many locales, formal approvals |
| Storyblok | Fully hosted | Visual editor with live preview | Marketers who assemble pages from blocks |
| WordPress (headless) | Your WordPress install, read over REST or GraphQL | The editor your team already knows | Teams trained on WordPress |
As of October 2026 all five can be started for free, either on a free plan or by self-hosting, and the paid plans are priced by seats, usage or both. Check the pricing page for your number of editors before deciding, because seat prices are where costs grow. Sanity vs Contentful vs Payload compares the three most common in more depth.
Choose with the editors in the room. A demo of the editing screen tells you more than the API documentation.
Step 3: How should you model the content?
Model content around page types, not pages. A "Case study" type might have a title, a client sector, a summary, a hero image with alt text, a list of results and a body. A "Service" type has its own fields. Each field maps to one part of the layout.
Two rules save rework later. Make SEO fields part of every type: a meta title, a meta description and an optional share image, so generateMetadata can read them. And make the URL slug a required, unique field that editors cannot change after publishing without a redirect.
Step 4: How do you fetch CMS content without slowing the site?
Fetch in server components, and cache every query. With Cache Components enabled (cacheComponents: true in next.config.ts, which create-next-app now sets by default), you mark a function with "use cache", give it a lifetime with cacheLife and label it with cacheTag:
// lib/cms.ts
import { cacheLife, cacheTag } from "next/cache";
export type Page = { slug: string; title: string; description: string; body: string };
export async function getPage(slug: string): Promise<Page | null> {
"use cache";
cacheLife("max");
cacheTag("pages", `page:${slug}`);
const res = await fetch(`${process.env.CMS_API_URL}/pages/${slug}`, {
headers: { Authorization: `Bearer ${process.env.CMS_API_TOKEN}` },
});
if (!res.ok) return null;
return res.json();
}
The max profile keeps the result for a long time, which is right for CMS content: nothing changes until an editor publishes, and Step 5 handles that. The Next.js revalidating guide recommends exactly this pairing of a long lifetime and a tag for CMS data.
Call getPage from the page component and from generateMetadata. Add generateStaticParams returning the known slugs so those pages are rendered at build time. The token stays on the server because this code only runs in server components; never give it a NEXT_PUBLIC_ prefix. To type the response, paste a sample into the JSON to TypeScript tool.
Step 5: How do edits go live without a rebuild?
Add a Route Handler that the CMS calls when content is published, and have it revalidate the tag:
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST(request: Request) {
if (request.headers.get("x-webhook-secret") !== process.env.CMS_WEBHOOK_SECRET) {
return new Response("Unauthorized", { status: 401 });
}
const { slug } = await request.json();
revalidateTag(slug ? `page:${slug}` : "pages", "max");
return Response.json({ revalidated: true });
}
In the CMS, create a webhook for publish and unpublish events that posts to https://your-site.com/api/revalidate, sends the secret in an x-webhook-secret header, and includes the slug in the body. Most CMSs let you shape the payload; the webhook payload formatter helps you check what yours sends.
With the max profile, the next visitor after a publish gets the old version while the new one renders in the background, and everyone after that gets the new one. If an editor must never see the old version, pass { expire: 0 } instead of "max", at the cost of one slower request.
Step 6: How do editors preview drafts?
Use Draft Mode. A Route Handler checks a secret from the CMS, calls (await draftMode()).enable() and redirects to the page. While the Draft Mode cookie is set, cached functions run fresh on every request, so the editor sees the latest content, and every other visitor still gets the cached page. If your CMS serves drafts from a separate endpoint or needs a preview token, read isEnabled from await draftMode() and switch the URL.
The Next.js Draft Mode guide has the full route code, including the check that prevents the preview link being used as an open redirect.
Once this is in place, the setup is the one we describe in why we build company websites with Next.js: editors work in a CMS, visitors get pre-rendered pages, and nobody waits for a deploy. If your CMS is WordPress, headless WordPress with Next.js covers the WordPress side of the connection.
Was this guide useful?