WordPress

Running WordPress headless with a Next.js frontend

WP

A long-time client loves WordPress's editorial workflow, the familiar block editor, the plugin ecosystem their content team already knew, but was unhappy with their theme's performance, a slow time-to-interactive and a Lighthouse score that had been sliding for years as plugins accumulated. Rather than replacing their CMS, which would have meant retraining an editorial team that had used WordPress happily for the better part of a decade, we decoupled it: WordPress stayed as the content backend via the REST API, and a new Next.js frontend replaced the old theme entirely.

Why decoupling instead of replacing

The client's editorial team kept their exact workflow, which made the migration politically easy in a way a full CMS replacement never would have been. This is easy to underrate from a purely technical perspective, but for a client whose day-to-day operations depend on their content team being productive from day one, being able to say "nothing changes about how you write and publish" removed what would otherwise have been the single biggest source of internal resistance to the project. Editorial staff kept the same login, the same block editor, the same media library, and the same publishing workflow they'd used for years.

On the technical side, decoupling let us solve the performance problem without inheriting any of WordPress's frontend rendering limitations. The Next.js frontend fetches content from WordPress's REST API at build time for static pages and via incremental static regeneration for content that updates more frequently, which meant the public-facing site could be served as fast, cacheable static output regardless of how much plugin weight remained on the WordPress side, since none of that weight was involved in serving requests to actual site visitors anymore.

What headless cost us

Preview mode for unpublished drafts took real custom work to replicate, since headless setups lose the built-in WordPress preview functionality that themes get for free. In a traditional WordPress theme, clicking "Preview" just renders the draft through the same theme templates the live site uses, because the theme and the content live in the same system. Once the frontend is a separate Next.js application, that link is broken, and we had to build a custom preview flow: a WordPress plugin that generates a signed preview token, and a corresponding Next.js API route that accepts the token and renders the draft content bypassing the normal static generation and caching path.

  • The client's editorial team kept their exact workflow, which made the migration politically easy in a way a full CMS replacement never would have been.
  • Preview mode for unpublished drafts took real custom work to replicate, since headless setups lose the built-in WordPress preview functionality that themes get for free.

Several WordPress plugins the client relied on also assumed they were rendering directly into a theme, and had to be re-evaluated or replaced. An SEO plugin that injected meta tags directly into theme output, for instance, needed its data pulled through the REST API instead and rendered by Next.js's own head management, which meant auditing every plugin the site depended on rather than assuming the migration would be a clean API swap with no frontend-side plugin dependencies to account for.

Rollout and what we'd tell a client considering this

We ran the two frontends in parallel behind a feature flag for about six weeks before fully cutting over, routing a small percentage of traffic to the new Next.js frontend and comparing analytics and error rates against the legacy theme before committing further. That overlap period caught two content-rendering edge cases, a custom Gutenberg block whose markup didn't translate cleanly through the REST API, and a shortcode pattern used in older posts that the new frontend didn't know how to parse, that would have been much more painful to discover after a full cutover with editorial staff already relying on the new system daily.

Handling content types the REST API doesn't expose cleanly by default

Custom post types and advanced custom fields, both used extensively in this client's WordPress setup, don't come through the default REST API in a particularly usable shape out of the box; ACF fields in particular are nested under a separate `acf` key with a shape that depends entirely on how the fields were configured in the WordPress admin, with no strong typing or validation on the Next.js side unless we built it ourselves. We wrote a thin normalization layer on the Next.js side that transforms the raw REST response into a well-typed internal shape before any component touches it, which meant that if an editor added a new ACF field, the normalization layer, not every individual component consuming that content type, is the one place that needed updating.

Caching and revalidation tuning

Getting the incremental static regeneration timing right took a few iterations. Setting the revalidation window too short meant unnecessary rebuild traffic hitting the WordPress REST API on pages that hadn't actually changed, which risked overloading the same backend we'd migrated away from precisely because of performance concerns. Setting it too long meant editors publishing time-sensitive content, an announcement banner, a pricing update, and finding the change hadn't propagated to the live site for longer than felt acceptable. We ended up implementing an on-demand revalidation webhook triggered directly from a WordPress publish action, which let us set a much longer default revalidation window for the common case while still getting near-immediate updates for the specific pages an editor was actively working on.

Search needed a completely different approach

WordPress's built-in search, and the plugin-based search improvements layered on top of it in the old theme, relied entirely on the theme rendering search results directly from a database query, a link that a decoupled frontend severs by definition. We ended up standing up a separate search index, populated via a webhook that fires whenever content is published or updated in WordPress, which the Next.js frontend queries independently of the main content API. This was more infrastructure than the client's original request implied, but it was a necessary consequence of decoupling that hadn't been obvious from the initial scoping conversation, and it's the kind of hidden cost we now flag explicitly during the sales and scoping process for any future headless WordPress project.

Editorial training turned out to be minimal, which was the whole point

Given how much engineering effort went into preserving the exact editorial workflow, it was genuinely satisfying to see the editorial team's actual training need turn out to be almost nothing. A single half-hour walkthrough covering the new preview button's slightly different location and confirming that publishing behaved identically to before was sufficient; there was no multi-week transition period of confused support tickets from an editorial team relearning their own CMS, which is precisely the outcome the entire architecture had been chosen to produce.

Cost and timeline realities worth setting expectations around

We were candid with the client early that a headless migration, despite preserving their familiar editorial workflow, is not a cheaper or faster project than a conventional theme redesign; it's a different shape of cost, less time spent on visual design iteration, more time spent on the API integration layer, the preview system, and search infrastructure that a traditional theme gets for free. Setting that expectation clearly during scoping avoided a mismatch partway through the project between what the client assumed "just a new frontend" would involve and what the architecture actually required to deliver the performance gains they were after.

Analytics also needed a second look after launch. Several of the client's existing tracking tags had been installed directly in the old theme's template files, a common pattern for a traditional WordPress setup, and had no equivalent hook to attach to once that theme was gone. We migrated tracking into a centralized tag-management approach on the Next.js side instead, which turned out to be more maintainable going forward than the original theme-embedded tags had ever been, since a marketing team member can now add or adjust tracking without needing a developer to touch template code at all.

Headless WordPress is a strong option when the editorial experience is non-negotiable but the public-facing performance needs a serious upgrade, and it's a much easier sell to a client than "let's replace your CMS," since it doesn't touch the tool their team already knows. Budget real time for preview functionality specifically; it's the piece most likely to be underestimated in a project plan, because it's invisible until an editor asks where the preview button went, and by then it's a blocker rather than a planned line item. We now budget for it explicitly on every headless WordPress project rather than treating it as an edge case to handle if time allows.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients