A headless CMS that fits Next.js instead of fighting it
Draft mode, ISR, the App Router and the render layer. What actually matters when you wire a headless CMS to Next.js — and the three ways it goes wrong.
Google's autocomplete for this topic is unusually consistent about what people
want. The three suggestions that surface most:
best headless cms for next js
free headless cms for nextjs
nextjs headless cms open source
Free, open source, and specifically for Next.js. This article is about what
separates the candidates once you have that shortlist — because the difference
that matters is not the SDK, it is where the content is when the request
arrives.
#Two architectures, and the one question that separates them
Inside your app. Payload runs within the Next.js application itself. You get
a local API that skips HTTP entirely, config-as-code, and one deploy. Excellent
developer experience for a single product.
The cost is coupling: CMS and website share a deploy, a runtime and a scaling
unit. A traffic spike on the public site is a spike on the thing your editors
are using, and changing frontend framework is no longer a frontend project.
Beside your app. Strapi, Directus, Sanity, corpusctl. The CMS is a separate
service. You fetch content over the network, which means you now own a caching
decision.
That caching decision is the whole integration:
When a visitor requests a page, where does the content come from — and how
old is it allowed to be?
Everything else in this article follows from your answer.
Fetch per request. Simple and correct, and your CMS is now on the critical
path of every page load. Its latency is your TTFB and its downtime is your
downtime. Fine for an admin dashboard, wrong for a marketing site.
ISR with a time window.revalidate: 60 and content is at most a minute
stale. Works, and it means editors watch a clock after publishing, plus every
window expiry is a cache miss someone pays for.
Pre-build, serve from cache. Content is rendered at publish time and served
as a static artefact. Fastest and cheapest — as long as something handles
invalidation when content changes.
Most teams start at one, move to two when the bill or the latency hurts, and
reach three by wiring on-demand revalidation to a webhook.
Option three is right, and its hard part is cache invalidation: the page is a
copy of a computation, and the computation could change at any moment.
corpusctl removes the problem rather than managing it, by making the address
itself immutable.
Publishing runs the build pipeline: content is validated, rendered and written
to a hash-addressed file in object storage, then the manifest is refreshed.
The client resolves the slug to a manifest shard locally, then fetches that
address from the nginx edge cache.
Because the address is a content hash:
The same address can never return different content.
It is safe to cache for thirty days.
Publishing writes a new address; nothing needs purging.
Two HTTP requests, both from cache. The read client carries no token, because
there is nothing to authorise: the content is already public and already built.
The manifest is sharded, so a space with ten thousand entries still downloads
one small file rather than a growing index.
#Draft preview: the part everyone wires last and regrets
Editors expect to see unpublished work. In Next.js that means a route which
validates a secret, enables draft mode, and redirects.
It is also a security hole in most hand-rolled implementations. Two mistakes,
both common:
Comparing the secret with ===. String comparison short-circuits on the
first differing character, which leaks its length and position through timing.
Use a constant-time comparison.
Redirecting to whatever slug says.?slug=https://evil.example.com turns
your preview endpoint into an open redirect on your own domain — useful for
phishing, and it will be found by a scanner.
@corpusctl/next handles both:
The secret comparison is constant-time, absolute URLs and protocol-relative
paths and backslash tricks are rejected, and the redirect target is the value
the CMS confirmed — not the one the request supplied. decision.reason is a
stable machine-readable value you can map to your own copy in any language.
Fetching content is an afternoon. Turning it into styled markup is the project,
and it is where most headless CMS adoption stalls.
If the client library ships opinionated markup, you spend that time overriding
it. If it ships nothing, you spend that time building it — but at least you are
building rather than fighting.
corpusctl takes the second position deliberately. Block renderers ship with
zero default styling — not one line of CSS. Every block type is
overridable. An unknown block type is skipped gracefully rather than throwing,
so a content model that gets ahead of the front end degrades instead of
breaking a page.
The core render tree is framework-agnostic; the React, Vue and Next.js adapters
are thin layers on top. The rule behind it: if rendering a block type
requires framework-specific code, the abstraction is in the wrong place.
Even with pre-built content you usually want the front end to know something
changed — to refresh a listing, warm a route, or bust a downstream cache.
corpusctl fires webhooks on publish, unpublish, update and delete. Deliveries
sit in a durable queue with exponential backoff and jitter, so an hour of
receiver downtime does not lose events. Payloads are HMAC-signed with a
timestamp, and the endpoint URL is checked against SSRF before it is ever
called — internal network addresses are rejected at registration.
Payload — one Next.js product, config-as-code, comfortable self-hosting.
Note that Cloud is not accepting new projects following the Figma acquisition.
Strapi — the largest community by a distance (~2,800 Stack Overflow
questions), free self-hosted. Strapi Cloud bills per project.
Sanity — best editing experience in the category, GROQ, and per-seat
pricing.
corpusctl — pre-built immutable reads with no meter, several projects from
one panel, zero-style renderers. Smaller community; no marketplace; the edge
cache is single-region.
Open source rarely means free to run. Licences, cloud upsells and the verified cost of six popular headless CMSs — with the numbers from their own pages.