Headless WordPress with Next.js: Lessons from a Real Migration

Six months ago I ripped out my WordPress theme and replaced it with a Next.js frontend. The WordPress backend stayed, serving content through the REST API. Here’s what I learned — the parts nobody talks about in tutorials.
Why Go Headless?
My WordPress theme was a React app bundled with Vite, served through PHP templates. It worked, but I was fighting WordPress at every turn — its routing wanted to control the page, its scripts enqueuing conflicted with Vite’s output, and server-side rendering was impossible. Going headless meant WordPress does what it’s good at (content management) while Next.js handles what it’s good at (rendering, routing, performance).
What Went Well
Performance jumped immediately. Static generation with ISR meant pages loaded from the CDN edge instead of hitting PHP on every request. Time to First Byte dropped from ~800ms to under 100ms for most pages.
Developer experience improved drastically. TypeScript everywhere, proper component composition, hot module replacement that actually works. No more juggling PHP templates and React entry points.
The WordPress REST API is surprisingly complete. Posts, pages, custom post types, taxonomies, menus, options — it’s all there. I added a few custom endpoints for specialized queries, but 90% of what I needed was built in:
// lib/wordpress.ts
const WP_API = process.env.WP_API_URL;
export async function getPosts(page = 1, perPage = 10) {
const res = await fetch(
`${WP_API}/wp/v2/posts?page=${page}&per_page=${perPage}&_embed`,
{ next: { revalidate: 300 } }
);
if (!res.ok) throw new Error('Failed to fetch posts');
return {
posts: await res.json(),
totalPages: parseInt(res.headers.get('X-WP-TotalPages') || '1'),
};
}
The Surprises
Preview links break. WordPress preview expects to render the post itself. With a headless setup, you need a custom preview route in Next.js that fetches draft content using authentication. I ended up building a /api/preview route that sets a preview cookie and redirects:
// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
const secret = searchParams.get('secret');
if (secret !== process.env.PREVIEW_SECRET) {
return new Response('Invalid token', { status: 401 });
}
(await draftMode()).enable();
redirect(`/blog/${slug}`);
}
WordPress plugins that inject frontend markup are useless. Contact Form 7, Yoast’s schema markup, any plugin that renders HTML — none of it reaches your Next.js frontend. You either rebuild that functionality in Next.js or find API-based alternatives. I switched to a headless form handler and built my own SEO metadata from the REST API response.
Image handling needs thought. WordPress stores images with its own URL structure. I use Next.js Image component with a custom loader that points to the WordPress media URLs, with remotePatterns configured:
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'wp.example.com',
pathname: '/wp-content/uploads/**',
},
],
},
};
SEO Considerations
The fear that headless hurts SEO is outdated. Next.js with server rendering produces fully-formed HTML that search engines can crawl. I actually saw ranking improvements because page speed improved so dramatically.
Generate your metadata from WordPress data:
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title.rendered,
description: post.excerpt.rendered.replace(/<[^>]+>/g, ''),
openGraph: {
images: [post._embedded?.['wp:featuredmedia']?.[0]?.source_url],
},
};
}
Was It Worth It?
Absolutely, but it’s not free. You’re maintaining two deployments (WordPress hosting + Vercel/Node), dealing with CORS, building features that plugins used to handle, and onboarding gets more complex. If your WordPress site is mostly static content with occasional updates, headless is a clear win. If you rely heavily on WordPress plugins for frontend functionality, think carefully about the migration cost.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


