How to Choose a CMS for a Next.js Project
How to choose a CMS for a Next.js project: score options on webhook revalidation, draft mode support, and typed schemas, with App Router code.
Pick a CMS for a Next.js project by testing three things. Can it clear your cache tags from a webhook? Can its API serve unpublished entries for draft mode? Is the schema typed? Feature checklists rank platforms. Your App Router setup picks one. Draftbase was built against that path, with typed schemas, webhooks on every entry event, and a REST or GraphQL delivery API you can tag per fetch.
Most "best CMS for Next.js projects" roundups list ten logos and a price column. None of the top results we read ship a single line of code. So here's the version with the code.
How do you pick a CMS for a Next.js project?
Score candidates on four things, in this order.
- Webhook to cache invalidation, end to end.
- Draft content over an API, for preview.
- Typed schema, ideally generated.
- Price at your entry count, published on the page.
Everything else is taste. A visual editor is nice. It won't save you when an editor fixes a typo and the page stays stale for six hours.
Rendering mode sets the shortlist
Next.js doesn't have one rendering mode. It has a per-route decision, and each one asks something different of your CMS. Pick the route strategy first. Then judge the CMS against it.
Static routes with on-demand revalidation
This is the common shape for marketing pages, docs, and blogs. Pages are prerendered. A webhook from the CMS marks them stale when content changes. The CMS needs two things here: reliable webhooks, and a payload you can map to a tag.
Dynamic routes that read per request
Dashboards, personalized pages, anything behind auth. Caching moves out of the picture, so the CMS's read latency becomes your TTFB. Ask for the p95 of the delivery API, not the marketing number.
Build-time fetch only
Fine for a 40-page site. It stops being fine when a rebuild takes eleven minutes and an editor wants a price changed now. If your candidate CMS only supports the full-rebuild flow, that's a real ceiling.
The framework choice sits upstream of all this. We covered it in how to choose a web framework. The React side lives on the React CMS pillar.
Can the CMS drive revalidateTag from a webhook?
Yes, if it fires a webhook with the entry id and type. That's the whole requirement. Tag your fetches, then invalidate the tag when the webhook lands.
// app/blog/[slug]/page.tsx
const res = await fetch(
`https://api.draftbase.co/delivery/entries?templateId=blogPost`,
{
headers: { Authorization: `Bearer ${process.env.DRAFTBASE_KEY}` },
next: { tags: ['blogPost'] },
},
);
Tags are case-sensitive and capped at 256 characters, per the Next.js revalidateTag reference. Use the template id. Don't build a tag out of a title.
The one-argument call is deprecated
Here's the part most Next.js CMS guides still get wrong. revalidateTag(tag) with a single argument is deprecated in Next.js 16. The docs say it works today only if you suppress the TypeScript error, and it may be removed.
The current signature takes a profile:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { templateId } = await request.json();
// Webhooks need immediate expiry, not stale-while-revalidate
revalidateTag(templateId, { expire: 0 });
return Response.json({ revalidated: true });
}
profile: "max" serves the old page while the new one builds behind it. That's the default the docs push. But an editor hitting publish is watching the page and waiting. For that, { expire: 0 } is the documented pattern.
updateTag won't work in a route handler
updateTag expires a tag immediately and blocks the next request until fresh data arrives. It sounds like what you want for a webhook. It isn't available there. Next.js restricts it to Server Actions and throws anywhere else:
Error: updateTag can only be called from within a Server Action
So a CMS webhook uses revalidateTag. An in-app edit form uses updateTag. Two different call sites, two different functions.
Does draft mode work with your CMS?
Only if the CMS API can return unpublished entries. Draft mode is a Next.js cache bypass, not a content feature. It can't invent content your API refuses to serve.
What Next.js gives you
Calling draft.enable() sets a cookie named __prerender_bypass. Requests that carry it skip the fetch cache. Components inside 'use cache' run again, and their output is not stored. The page comes back with Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate. Other visitors keep the cached page.
// 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);
if (searchParams.get('secret') !== process.env.DRAFT_SECRET) {
return new Response('Invalid token', { status: 401 });
}
const post = await getPostBySlug(searchParams.get('slug'));
if (!post) return new Response('Invalid slug', { status: 401 });
const draft = await draftMode();
draft.enable();
redirect(post.slug);
}
One gotcha the docs call out and the roundups skip: redirect to the slug you looked up in the CMS, never the one from searchParams. Otherwise the preview endpoint is an open redirect.
What the CMS has to provide
A preview URL template, an API that reads drafts, and a token to authenticate it. Ask candidates for all three by name.
Draftbase splits this on purpose. The delivery API serves published content only. Edit an entry after publishing and the API keeps returning the last published version, not the pending edit. Drafts come from the management API, which is what your /api/draft handler calls. So a half-finished paragraph can't reach your live site by accident. Know the split before you wire preview.
Free CMS options for Next.js: read the metering, not the tier name
A free CMS for Next.js is easy to find and easy to outgrow. What matters is which axis meters you.
| Metered on | Hits you when | Typical warning sign |
|---|---|---|
| API requests | Traffic grows, even with caching | Overage billing per 10k calls |
| Entries | A docs site passes a few hundred pages | Hard cap, not a soft limit |
| Seats | The third editor joins | Per-seat pricing on the cheapest paid tier |
| Environments | You add staging | Staging counts as a full project |
Draftbase meters entries, media, storage, environments, and seats. Hobby is free. Startup is $49/mo, and the number is on the pricing page rather than behind a call.
Headless CMS comparison for Next.js: what to actually test
Build a throwaway route. Not a spreadsheet. An hour of prototyping tells you more than any comparison post, this one included.
- Publish an entry, time how long until the static page updates.
- Break the webhook secret on purpose. Does the CMS retry, or drop it?
- Open a draft preview in an incognito window. Does it leak to anonymous traffic?
- Rename a field. Does your build fail, or does the page render
undefined?
That last one separates typed CMSs from JSON blobs. With a generated client, a renamed field is a compile error. Without one, it's a support ticket next Tuesday.
Is App Router caching hard to get right?
No. It's just easy to get wrong once and never notice.
There are two moving parts. You tag a read. You clear the tag when the source changes. That's it. The trouble is that a stale page looks fine. Nothing throws. No alert fires. The copy is simply old.
So test the loop, not the parts. Publish a change. Reload the page. Time it. If the new text shows up in under a second, your wiring is sound. If it takes six hours, your webhook never landed and your cache is running on a timer.
Do this on day one of the trial. Not in week three, after the schema is built.
What goes wrong in production
Three failures show up again and again, and none of them are the CMS's fault on paper.
The first is a dropped webhook. The CMS fired it. Your route was cold, or down, or the deploy was mid-swap. The page stays stale and nobody knows. Log every call to your revalidate route, and check whether the CMS retries. If it doesn't, you need a nightly sweep.
The second is a tag that no longer matches. Someone tags a fetch blog-posts and the webhook sends blogPost. Tags are case-sensitive, so nothing throws. The page just never updates. Derive both sides from one shared constant.
The third is preview leaking. A shared draft URL gets pasted in Slack, and the cookie rides along. Short-lived secrets fix it. So does an exit form in the layout.
The underused angle: preview is an auth problem
Draft mode gets treated as a preview feature. It's an access-control feature wearing a preview costume.
Three things are true at once. The __prerender_bypass cookie is a bearer token for unpublished content. The /api/draft route is a public endpoint until you check the secret. And the CMS's draft API key, if it lives in the same env file as the delivery key, has a much wider blast radius.
Scope the draft token to read-only. Rotate it on offboarding. And check the redirect target, because the open-redirect path above is the one real vulnerability in an otherwise boring integration. None of the ten roundups we read mention any of this.
When is a headless CMS the wrong choice for Next.js?
When there's no content. A five-page site with copy that changes twice a year does not need an API, a webhook handler, and a preview route. Put the words in MDX files in the repo and move on. You can migrate later, and the migration guide exists for exactly that.
It's also the wrong choice when your editors need a visual page builder and nobody on the team wants to build one. Draftbase gives editors a typed form UI, not drag-and-drop layout. If your marketing team expects to move a hero section by dragging it, say so during evaluation. Prismic and Storyblok are honest answers to that requirement, and pretending otherwise wastes a month.
Where Draftbase fits
If you're building a content-driven Next.js app and your editors are fine with structured forms, the fit is direct. Webhooks fire on every entry event, so your revalidateTag handler has something to listen to. Rich text is stored as plain MDX strings, so registered components with typed props render through @draftbase/renderer in React Server Components without a vendor tree in the middle. Revisions roll back when a bad edit ships.
Start on Hobby, wire one route, and break the webhook on purpose before you commit. That test takes an afternoon and it's the only comparison that counts.
How to
- 1Pick the rendering mode per route
Decide which routes are static, which read per request, and which are built once. That choice sets the shortlist before any CMS feature matters.
- 2Tag every content fetch
Pass next.tags to fetch, or call cacheTag inside a use cache function. Use the template id so a webhook payload maps to it directly.
- 3Build the revalidate route handler
Read the entry type from the webhook body and call revalidateTag with an expire of 0. Log every call so a dropped webhook is visible.
- 4Wire draft mode behind a secret
Add a route that checks a shared token, looks the entry up in the CMS, calls draft.enable, then redirects to the slug it looked up.
- 5Break it on purpose
Publish an edit and time the update. Then send a bad secret and a stale tag, and confirm both fail loudly instead of silently.
Ship content that's built to be found
Draftbase generates schema, structured data, and a fast MDX editor for every post.
Frequently asked questions
What is the best CMS for Next.js projects?
The one whose webhooks can clear your cache tags and whose API serves drafts. Payload, Sanity, Storyblok and Draftbase all do both. Test the loop in an hour before you pick.
Is there a free CMS for Next.js?
Yes. Draftbase, Strapi, Payload and Sanity all have free tiers. Check what meters you first. Entry caps, seat pricing and per-environment billing bite at different times.
Does a headless CMS work with the Next.js App Router?
Yes, if it fires webhooks you can map to a cache tag. Tag your fetch with next.tags, then call revalidateTag from a route handler when content changes.
Why does my Next.js page still show old content?
Your cache tag never got cleared. Check that the webhook reached your route, and that the tag string matches on both sides. Tags are case-sensitive.
Do I need draft mode to preview CMS content?
Yes, for cached routes. Draft mode sets a cookie that skips the cache. Your CMS also needs an API that returns unpublished entries, or there is nothing to preview.