Skip to main content
Back to blog

Migrating a Legacy WordPress Site to a Headless Architecture

What actually changes — and what doesn't — when you move a content-heavy WordPress site to a headless setup with a REST/GraphQL frontend.

3 min read

"Headless WordPress" gets pitched as a silver bullet for performance and developer experience. It can be — but only if the migration is scoped honestly. Here's what I've learned moving a content-heavy, plugin-dependent WordPress site to a headless architecture without a rebuild-from-scratch budget.

Decide what "headless" means for this project #

There's a spectrum, not a binary:

  1. Decoupled front, WordPress admin stays. Editors keep using wp-admin; the public site is a separate frontend consuming the WordPress REST or GraphQL API. This is the most common and lowest-risk option.
  2. Fully headless. WordPress becomes a pure content API with no public-facing theme at all.
  3. Hybrid. Some routes stay server-rendered by WordPress (checkout, account pages from plugins that assume a WordPress frontend); everything else is decoupled.

Most of my migrations land on option 1 or 3 — a full headless rebuild only pays off when the editorial team's workflow genuinely benefits and the plugin ecosystem in use doesn't assume a traditional theme is rendering the page.

Audit what breaks first #

Before touching the frontend, inventory every plugin that renders its own markup directly into the page — SEO plugins, forms, page builders, membership/paywall plugins. These either need a headless-compatible replacement or a plan to keep their functionality server-side behind the API.

The SEO plugin trap

Most SEO plugins (Yoast, Rank Math) generate meta tags via wp_head(), which a headless frontend never renders. Use their REST API exposure instead — Yoast exposes an _links.yoast_head_json field per post that gives you the fully computed title, description, and structured data to render in your frontend's <head>.

Expose only what the frontend needs #

The default WP REST API response is chatty and leaks internal fields. I add a custom namespace rather than over-fetching from /wp/v2/:

add_action( 'rest_api_init', function () {
    register_rest_route( 'site/v1', '/posts', [
        'methods'  => 'GET',
        'callback' => 'site_get_posts_for_frontend',
        'permission_callback' => '__return_true',
    ] );
} );
 
function site_get_posts_for_frontend( WP_REST_Request $request ) {
    $posts = get_posts( [ 'posts_per_page' => 10 ] );
 
    return array_map( function ( $post ) {
        return [
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'slug'  => $post->post_name,
            'excerpt' => get_the_excerpt( $post ),
            'cover' => get_the_post_thumbnail_url( $post, 'large' ),
        ];
    }, $posts );
}

This keeps the API surface intentional, cacheable, and easy to version — instead of the frontend depending on WordPress's internal response shape.

Preview and draft content #

Editors expect to preview unpublished changes. With a decoupled frontend this needs explicit wiring: a signed preview token from WordPress that the frontend uses to request draft content and bypass any public caching for that request.

Caching moves, it doesn't disappear #

In a traditional WordPress setup, page caching happens at the web server or a caching plugin. Headless shifts this responsibility to the frontend/CDN layer — but WordPress still needs to notify the frontend when content changes, typically via a webhook that triggers on-demand revalidation (Next.js's revalidatePath/revalidateTag, for example) rather than a fixed TTL.

Lessons learned #

The migrations that went smoothly were the ones where I mapped every plugin's rendering dependency before writing frontend code. The ones that went over budget were the ones where "just point the frontend at /wp-json/" turned out to ignore three plugins that assumed a WordPress-rendered page existed.

A practical WordPress deployment pipeline: Docker for environment parity, automated checks in CI, and zero-downtime releases.

A practical comparison of Algolia and Solr for product search — cost, relevance tuning, operational overhead, and when each one wins.

Practical techniques for keeping WooCommerce fast under real traffic — from database indexing and object caching to queue-based order processing.