Headless WordPress that editors kept using
A multi-locale marketing site moved onto a Next.js frontend with WordPress as the editing layer — including the preview and publishing pieces that headless builds usually drop.
- Role
- Lead engineer · Multi-brand retail client
- Stack
- WordPressPHPNext.jsTypeScriptREST APIISR
- Content modelled as typed fields rather than HTML blobs
- Live preview restored, so editors could see drafts before publishing
- Publish-to-visible in seconds via tag-based revalidation, not a rebuild
The problem
The marketing site ran on a classic WordPress theme that had absorbed years of plugins. Pages were slow, the theme was too tangled to change safely, and every layout adjustment turned into a PHP debugging session.
Going headless was the obvious move and the obvious trap. The theme was carrying more than rendering — it was carrying preview. Take it away and the editors, who were the reason the CMS existed, are publishing blind into a build pipeline they cannot see into.
So the project was only half a frontend rebuild. The other half was putting back the things headless quietly removes.
Modelling the content
The first job was getting out of post_content. A single HTML blob per page
cannot be restyled, validated, or rendered anywhere that isn't a browser — and it
is why the old theme had become load-bearing.
add_action( 'init', function () {
register_post_type( 'campaign', [
'public' => true,
'show_in_rest' => true, // non-negotiable: REST is the only consumer now
'supports' => [ 'title', 'excerpt', 'thumbnail', 'revisions' ],
'rewrite' => [ 'slug' => 'campaigns' ],
] );
// Anything the frontend reads must be registered and visible in REST.
// Plugin-only fields that never reach the API are the classic source of
// "it looks right in the admin but the site doesn't show it".
register_post_meta( 'campaign', 'headline', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
// Enforced here, because the alternative is a note in a document
// nobody reads and a headline that wraps to four lines on mobile.
'auth_callback' => '__return_true',
] );
} );For page composition we used Gutenberg, but a closed set of it. Custom blocks the
frontend knows how to render, everything else disabled via allowedBlockTypes.
Giving preview back
WordPress's Preview button renders the theme. With no theme it either 404s or shows something unrelated to production, and from the editor's chair the site has become a black box.
The fix was a preview route on the Next.js side that fetches draft content by id and renders it through the real components, plus a filter redirecting WordPress's preview link at it. An afternoon of work, and the single thing that most decided whether editors trusted the new setup or went back to asking engineers.
Publishing without rebuilding
Rebuilding the whole site per content change works until there are a few hundred pages, then the gap between pressing Publish and seeing the change grows until nobody believes the button did anything.
A webhook on publish invalidates the specific cache tags for that content. One page changed, one page revalidated, a couple of seconds. Multiply that by the locales and it still holds, because invalidation is per-entry rather than global.
Localisation
Locales rarely mean fully separate content. Most markets inherit a regional base and override a handful of fields. Modelling that inheritance explicitly — rather than duplicating a post per market — meant a global copy change propagated everywhere it hadn't been deliberately overridden, and the override was visible in the editor rather than implied by a duplicate.
Outcome
Editors kept the interface they already knew and got a preview that matches production. The frontend became an ordinary Next.js codebase with no theme constraints. Routine content changes stopped reaching engineering.
What made the difference was not the rendering layer — that part is straightforward. It was the field registration, the preview route, and the invalidation strategy: the three pieces headless projects skip, and the three that decide whether the CMS is still used six months later.