Skip to main content
Back to Articles
WordPressSeptember 1, 20258 min read

WordPress to Headless CMS: Migration Playbook

Step-by-step migration guide from traditional WordPress to a headless architecture with WP REST API, Next.js frontend, and optimized hosting.

Why Migrate to Headless WordPress

Traditional WordPress renders pages server-side through PHP themes. While this works well for simple sites, it creates performance bottlenecks at scale: every page request executes PHP, queries the database, and renders templates. Plugin bloat compounds the problem — each active plugin hooks into the request lifecycle.

Headless WordPress decouples the CMS from the frontend. WordPress becomes a content management API, and a modern frontend framework handles rendering. The result is dramatically faster page loads, better security (the WordPress admin is not publicly accessible), and a superior developer experience.

This playbook covers the complete migration path, based on headless WordPress projects where we handle WordPress performance optimization and infrastructure.

Pre-Migration Assessment

Before migrating, audit your current WordPress installation:

Content Inventory

SELECT post_type, COUNT(*) as count
FROM wp_posts
WHERE post_status = 'publish'
GROUP BY post_type
ORDER BY count DESC;

Document every content type, custom field, taxonomy, and relationship. Custom post types and ACF (Advanced Custom Fields) groups need explicit API exposure.

Plugin Dependency Analysis

Categorize plugins into three groups:

  • Content plugins (ACF, WPML, Yoast): These affect content structure and need API-side equivalents
  • Frontend plugins (Elementor, WPBakery): These become unnecessary in headless mode
  • Backend plugins (WooCommerce, Gravity Forms): These need API endpoints or webhook integration

Setting Up the WordPress API

Exposing Custom Fields

// functions.php
add_action('rest_api_init', function () {
    register_rest_field('post', 'acf_fields', [
        'get_callback' => function ($post) {
            return get_fields($post['id']) ?: [];
        },
        'schema' => [
            'description' => 'ACF fields for this post',
            'type' => 'object',
        ],
    ]);
});

Custom Endpoints for Performance

The default WP REST API returns more data than frontends typically need. Create lean custom endpoints:

add_action('rest_api_init', function () {
    register_rest_route('headless/v1', '/posts', [
        'methods' => 'GET',
        'callback' => function (WP_REST_Request $request) {
            $page = $request->get_param('page') ?: 1;
            $per_page = $request->get_param('per_page') ?: 12;

            $query = new WP_Query([
                'post_type' => 'post',
                'posts_per_page' => $per_page,
                'paged' => $page,
                'post_status' => 'publish',
            ]);

            $posts = array_map(function ($post) {
                return [
                    'id' => $post->ID,
                    'slug' => $post->post_name,
                    'title' => $post->post_title,
                    'excerpt' => wp_trim_words($post->post_content, 30),
                    'date' => $post->post_date,
                    'image' => get_the_post_thumbnail_url($post->ID, 'large'),
                    'categories' => wp_get_post_categories($post->ID, ['fields' => 'names']),
                ];
            }, $query->posts);

            return new WP_REST_Response([
                'posts' => $posts,
                'total' => $query->found_posts,
                'pages' => $query->max_num_pages,
            ]);
        },
        'permission_callback' => '__return_true',
    ]);
});

Building the Next.js Frontend

Data Fetching Layer

// lib/wordpress.ts
const WP_API_URL = process.env.WORDPRESS_API_URL!;

export async function getPosts(page = 1, perPage = 12) {
  const res = await fetch(
    `${WP_API_URL}/headless/v1/posts?page=${page}&per_page=${perPage}`,
    { next: { revalidate: 60 } }
  );
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

export async function getPost(slug: string) {
  const res = await fetch(
    `${WP_API_URL}/headless/v1/posts/${slug}`,
    { next: { revalidate: 300 } }
  );
  if (!res.ok) throw new Error("Post not found");
  return res.json();
}

Incremental Static Regeneration

Use ISR to serve statically generated pages that revalidate in the background:

// app/blog/[slug]/page.tsx
export const revalidate = 300;

export async function generateStaticParams() {
  const { posts } = await getPosts(1, 100);
  return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}

Content Preview System

Editors need to preview unpublished content. Implement a preview mode:

// WordPress: Generate preview links for the headless frontend
add_filter('preview_post_link', function ($link, $post) {
    $frontend_url = 'https://example.com';
    $token = wp_create_nonce('wp_rest');
    return "{$frontend_url}/api/preview?id={$post->ID}&token={$token}";
}, 10, 2);

Infrastructure Setup

The headless architecture requires separate hosting for the WordPress API and the Next.js frontend:

WordPress Backend: Host on a managed server with PHP-FPM, MariaDB, and Redis object cache. Restrict public access to the REST API endpoints only — block wp-admin access to internal IPs.

Next.js Frontend: Deploy on Vercel or a Kubernetes cluster with edge caching. The frontend makes API calls to the WordPress backend during build and on-demand revalidation.

CDN Layer: CloudFront or Cloudflare in front of both services, with aggressive caching for static assets and API responses.

Migration Checklist

  • Export content inventory and validate all custom fields are API-accessible
  • Set up API authentication with application passwords or JWT
  • Build and test all frontend pages against the API
  • Configure webhook-based revalidation for content updates
  • Implement 301 redirects for changed URL structures
  • Run parallel sites for two weeks before DNS cutover
  • Disable the WordPress frontend theme after cutover

Performance Results

Typical improvements after headless migration:

  • TTFB: 800ms to under 100ms
  • LCP: 3.5s to under 1.2s
  • Server costs: 40 percent reduction (WordPress no longer renders pages)
  • Build time: under 3 minutes for 1,000+ pages with ISR

The migration effort pays for itself within months through better Core Web Vitals, higher search rankings, and reduced hosting costs.

Need help with this?

Our team handles this kind of work daily. Let us take care of your infrastructure.