Headless WordPress without the regret
Going headless solves a rendering problem and creates an editing one. Most of the disappointment comes from taking the frontend away without giving anything back.
- wordpress
- cms
- next.js
Headless WordPress gets pitched as a straight upgrade: keep the admin everyone knows, replace the theme layer with something modern, ship a fast site. The first half is true. The second half is where projects go wrong.
What actually happens is that you take away the editor's preview, their page builder, and their ability to see what they are publishing — and hand back a build pipeline. That trade is worth making, but only if you put the missing pieces back deliberately.
Decide what WordPress is for
The projects that go badly are the ones that never answered this. WordPress ends up half content store and half application, and nobody can say which side owns what.
The version that works: WordPress is the editing interface and the content store. It is not the router, the template layer, or the source of design decisions. Anything that renders is the frontend's job.
That sounds obvious until a plugin wants to inject markup, or a shortcode produces HTML that only makes sense inside a theme. Both are the boundary being crossed, and both are worth refusing early — a shortcode that emits a styled block is a template hiding inside your content store.
Register content properly, not as free-form HTML
The default WordPress editing model — one big post_content blob of HTML — is
the single biggest obstacle to a headless build. You cannot restyle it, you
cannot validate it, and you cannot render it anywhere that isn't a browser.
Custom post types with explicit fields turn content back into data:
add_action( 'init', function () {
register_post_type( 'case_study', [
'public' => true,
'show_in_rest' => true, // required — the REST API is the whole point
'supports' => [ 'title', 'excerpt', 'thumbnail', 'custom-fields' ],
'rewrite' => [ 'slug' => 'work' ],
] );
// Registered fields appear in REST and can be validated. A field the
// frontend depends on should never be an ACF-only field that the API
// does not know about.
register_post_meta( 'case_study', 'client_name', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
] );
} );The rule I apply: if the frontend reads it, it must be a registered field visible in REST. Fields that exist only in a plugin's own storage are the classic source of it works in the admin but the API doesn't return it.
Gutenberg blocks are a content model, if you let them
If the client needs flexible page composition — and they usually do — blocks are the right primitive, provided you constrain them.
A closed set of blocks that your frontend knows how to render is a content model
with a visual editor attached. An open set, plus whatever a plugin adds, is an
open-ended HTML problem you will be solving forever. Register your own blocks,
disable the ones you cannot render, and set allowedBlockTypes so an editor
cannot compose a page the site cannot display.
Give the preview back
This is the piece most often skipped, and the one that decides whether editors trust the system.
WordPress preview works by rendering the theme. Remove the theme and Preview either 404s or shows something that looks nothing like production. From the editor's side, the site has become a black box they publish into and hope.
The fix is a preview route on the frontend that reads draft content by id and renders it with the real components, plus a filter pointing WordPress's preview link at it. It is an afternoon of work and it is the difference between a CMS people use and one they route around by asking an engineer.
Invalidate on publish, don't rebuild
Rebuilding the whole site on every content change works until there are a few hundred pages, then it doesn't — and the gap between Publish and visible grows until editors stop believing the button worked.
Tag-based revalidation triggered by a WordPress webhook is what keeps that gap at a couple of seconds regardless of site size. One page changed, one page invalidated.
What you actually get
Done properly: editors keep an interface they already know, the frontend is whatever you want it to be, and the render layer stops being hostage to the theme's assumptions.
Done carelessly: a slower editing experience, no preview, a build that takes twenty minutes, and a team that files tickets instead of publishing — which is precisely the problem the CMS was there to solve.
The difference is almost entirely in the parts that aren't the fun bit: the field registration, the preview route, and the invalidation.