Building a headless WordPress frontend is the best way to get modern performance without abandoning the CMS your content team already knows.
WordPress as a headless CMS is underrated. The REST API has been stable since WordPress 4.7. The admin UI is familiar to every content editor on the planet. Plugins handle SEO, forms, e-commerce, and custom fields. With 60,000+ plugins and the largest CMS market share on the web, you’re never short on integrations.
The hard part was never the backend. It’s the frontend.
You decouple WordPress, build a React or Astro frontend, and then you’re stuck figuring out where to host it, how to handle routing, and how to deal with preview mode. I’ve been through this loop enough times to map the whole thing out. This guide covers the full process – from setting up the WordPress REST API to deploying a headless frontend to production.
Why Go Headless with WordPress
The traditional WordPress stack serves HTML from PHP on every request. It works, but it comes with tradeoffs: slow page loads (a typical WordPress page makes 20-50 database queries), security surface area from themes and plugins, and a templating system built for 2005.
Going headless means WordPress handles content only. A separate frontend – built with whatever framework you want – handles presentation.
What you gain:
- Performance. Static or server-rendered pages load 2-5x faster than PHP-generated WordPress pages. No database query on every page view.
- Security. Your public-facing site is static HTML or a Node.js app. The WordPress admin lives on a separate URL, invisible to visitors. That means fewer entry points for attackers – no exposed
wp-login.phpon the public domain. - Modern DX. Use React, Vue, Svelte, or whatever your team knows. Component-based architecture. Hot reload. TypeScript. The full modern frontend toolchain.
- Flexibility. Same content, multiple frontends. A marketing site, a mobile app, and a docs portal can all pull from one WordPress backend.
- Hosting freedom. WordPress stays on traditional PHP hosting. The frontend can go anywhere – a CDN, a Node.js server, an edge network.
What you lose:
- Live preview (requires extra setup)
- Theme ecosystem (you’re building your own frontend)
- Some plugins that render frontend HTML (contact forms, page builders)
- The simplicity of a single deploy
For content-heavy sites with a dev team, the tradeoff is usually worth it.
Architecture Overview
A headless WordPress setup has three pieces:
WordPress (CMS) → API → Frontend (React/Astro/Vue) → Hosting
WordPress handles content creation, user management, and editorial workflow. It exposes content through either the REST API (built-in) or WPGraphQL (plugin).
The API layer is where your frontend fetches data. Two options:
- REST API – Built into WordPress core. No plugins needed. Returns JSON. Endpoints like
/wp-json/wp/v2/postsand/wp-json/wp/v2/pages. - WPGraphQL – Plugin that adds a GraphQL endpoint. More efficient queries (fetch only what you need), but requires an extra plugin.
For this guide, I’m using the REST API since it requires zero WordPress setup.
The frontend is a standalone app that fetches content from the API and renders it. This is where you choose your framework.
Choosing a Frontend Framework
Four frameworks dominate the headless WordPress space in 2026. Each has a different sweet spot.
Next.js
The most popular choice. Next.js gives you static site generation (SSG), server-side rendering (SSR), and incremental static regeneration (ISR) – meaning you can rebuild individual pages when content changes without rebuilding the entire site.
Best for: Full-featured sites that need dynamic content, authentication, or server-side logic alongside WordPress content.
Rendering: SSG for blog posts, SSR for personalized pages, ISR for content that updates frequently.
Ecosystem: Largest community for headless WordPress. Most tutorials and starter templates target Next.js.
Astro
The fastest option for content sites. Astro ships zero JavaScript by default and only hydrates the interactive components you mark. A headless WordPress blog built with Astro will typically score 95-100 on Lighthouse with minimal optimization.
Best for: Blogs, marketing sites, documentation – anywhere performance matters more than interactivity.
Rendering: Static by default. Partial hydration for interactive islands.
Nuxt
The Vue equivalent of Next.js. If your team works in Vue, Nuxt gives you the same SSG/SSR capabilities with Vue’s composition API.
Best for: Vue teams building headless WordPress frontends.
SvelteKit
Lighter than Next.js with excellent performance out of the box. SvelteKit compiles away the framework, so the shipped JavaScript is minimal.
Best for: Developers who prefer Svelte’s syntax and want a smaller bundle size than React-based options.
| Framework | Language | Rendering | Best For | WP Community |
|---|---|---|---|---|
| Next.js | React | SSG, SSR, ISR | Full-featured sites | Largest |
| Astro | Any (or none) | Static, islands | Content/blog sites | Growing |
| Nuxt | Vue | SSG, SSR | Vue teams | Medium |
| SvelteKit | Svelte | SSG, SSR | Minimal JS bundles | Small |
Best Frontend Framework for Headless WordPress in 2026
If you’re starting a headless WordPress project today, here’s the short version:
- Choose Next.js if you need SSR, ISR, or server-side logic alongside your content. Largest community and most tutorials for headless WordPress.
- Choose Astro if you’re building a content-heavy site (blog, docs, marketing) where performance matters most. Zero JS by default, 95-100 Lighthouse scores.
- Choose Nuxt if your team already works in Vue. Same capabilities as Next.js with Vue’s ecosystem.
- Choose SvelteKit if you want minimal JavaScript bundles and prefer Svelte’s syntax.
For most headless WordPress projects in 2026, Next.js or Astro are the two strongest choices. Next.js wins on flexibility (SSR + ISR + API routes). Astro wins on speed (zero JS, static by default).
I’ll use Next.js for the step-by-step section since it supports all three rendering modes (SSG + SSR + ISR), but the WordPress API calls work the same regardless of framework.
Step-by-Step: Build a Headless WordPress Frontend with Next.js
1. Set Up Your WordPress Backend
You need a WordPress instance with the REST API accessible. If you’re using InstaWP, spin up a new site – the REST API is enabled by default and you can have a WordPress backend running in under a minute.
Verify it’s working by hitting this URL in your browser:
https://your-wp-site.com/wp-json/wp/v2/posts
You should see JSON output with your posts. If you get a 404, check that pretty permalinks are enabled (Settings > Permalinks > anything other than “Plain”).
Optional but recommended plugins:
- Advanced Custom Fields (ACF) – Exposes custom fields to the REST API
- Yoast SEO – Adds SEO metadata to API responses
- Application Passwords – Built into WP core since 5.6. Needed if you want authenticated API access for drafts or private content.
2. Create the Next.js Project
npx create-next-app@latest wp-frontend --typescript --tailwind --app
cd wp-frontend
Add your WordPress URL to .env.local:
WORDPRESS_API_URL=https://your-wp-site.com/wp-json/wp/v2
3. Create a WordPress API Client
Create lib/wordpress.ts to centralize all API calls:
const API_URL = process.env.WORDPRESS_API_URL;
export interface WPPost {
id: number;
slug: string;
title: { rendered: string };
content: { rendered: string };
excerpt: { rendered: string };
date: string;
featured_media: number;
_embedded?: {
"wp:featuredmedia"?: Array<{
source_url: string;
alt_text: string;
}>;
};
}
export async function getPosts(perPage = 10): Promise<WPPost[]> {
const res = await fetch(
`${API_URL}/posts?per_page=${perPage}&_embed`,
{ next: { revalidate: 60 } }
);
if (!res.ok) {
throw new Error(`Failed to fetch posts: ${res.status}`);
}
return res.json();
}
export async function getPostBySlug(slug: string): Promise<WPPost | null> {
const res = await fetch(
`${API_URL}/posts?slug=${encodeURIComponent(slug)}&_embed`,
{ next: { revalidate: 60 } }
);
if (!res.ok) {
throw new Error(`Failed to fetch post: ${res.status}`);
}
const posts = await res.json();
return posts.length > 0 ? posts[0] : null;
}
export async function getPages(): Promise<WPPost[]> {
const res = await fetch(
`${API_URL}/pages?_embed`,
{ next: { revalidate: 60 } }
);
if (!res.ok) {
throw new Error(`Failed to fetch pages: ${res.status}`);
}
return res.json();
}
A few things to note:
- The
_embedparameter tells WordPress to include featured images and author data inline, saving you extra API calls. next: { revalidate: 60 }enables ISR – Next.js will cache the page and revalidate it every 60 seconds. Change this to suit your publishing frequency.- The WordPress REST API returns rendered HTML in
title.renderedandcontent.rendered. You don’t need a markdown parser.
4. Build the Blog Index Page
Create app/blog/page.tsx:
import { getPosts } from "@/lib/wordpress";
import Link from "next/link";
export const metadata = {
title: "Blog",
description: "Latest posts from our blog",
};
export default async function BlogPage() {
const posts = await getPosts();
return (
<main className="max-w-3xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="space-y-8">
{posts.map((post) => (
<article key={post.id} className="border-b pb-8">
<Link href={`/blog/${post.slug}`}>
<h2 className="text-2xl font-semibold hover:text-blue-600">
{post.title.rendered}
</h2>
</Link>
<time className="text-gray-500 text-sm">
{new Date(post.date).toLocaleDateString()}
</time>
<div
className="mt-2 text-gray-700"
dangerouslySetInnerHTML={{
__html: post.excerpt.rendered,
}}
/>
</article>
))}
</div>
</main>
);
}
5. Build the Single Post Page
Create app/blog/[slug]/page.tsx:
import { getPostBySlug, getPosts } from "@/lib/wordpress";
import { notFound } from "next/navigation";
import { Metadata } from "next";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
// Adjust the limit based on your site size.
// Posts beyond this limit are rendered on-demand and cached.
const posts = await getPosts(100);
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return { title: "Post Not Found" };
return {
title: post.title.rendered,
description: post.excerpt.rendered.replace(/<[^>]*>/g, ""),
};
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
const featuredImage = post._embedded?.["wp:featuredmedia"]?.[0];
return (
<article className="max-w-3xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-4">
{post.title.rendered}
</h1>
<time className="text-gray-500">
{new Date(post.date).toLocaleDateString()}
</time>
{featuredImage && (
<img
src={featuredImage.source_url}
alt={featuredImage.alt_text}
className="w-full rounded-lg mt-6"
/>
)}
<div
className="prose prose-lg mt-8"
dangerouslySetInnerHTML={{
__html: post.content.rendered,
}}
/>
</article>
);
}
Key details:
generateStaticParamspre-renders all posts at build time (SSG). New posts get server-rendered on first visit, then cached via ISR.generateMetadatapulls the title and excerpt for SEO meta tags – critical for headless WordPress since you lose Yoast’s frontend rendering.- The
_embeddeddata includes featured images without a second API call.
6. Handle SEO
This is where headless WordPress gets tricky. In a traditional setup, Yoast handles everything. In a headless setup, you need to handle it yourself.
The basics that Next.js gives you for free:
generateMetadataon each page for title and description- Automatic
<html lang>attribute - Built-in
<Link>prefetching for fast navigation
What you need to add:
// app/layout.tsx - add to your root layout
export const metadata = {
metadataBase: new URL("https://yourdomain.com"),
openGraph: {
type: "website",
locale: "en_US",
siteName: "Your Site Name",
},
twitter: {
card: "summary_large_image",
},
};
Sitemap generation – create app/sitemap.ts:
import { getPosts } from "@/lib/wordpress";
export default async function sitemap() {
const posts = await getPosts(100);
const postUrls = posts.map((post) => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: new Date(post.date),
}));
return [
{ url: "https://yourdomain.com", lastModified: new Date() },
{ url: "https://yourdomain.com/blog", lastModified: new Date() },
...postUrls,
];
}
If you’re using the Yoast REST API extension, you can pull full SEO metadata (canonical URLs, OG images, schema markup) from WordPress and pass it through generateMetadata. This gives you the best of both worlds – editors control SEO in the familiar Yoast interface, and your frontend renders it correctly.
Headless WordPress Hosting in 2026
A headless WordPress setup has two things to host: the WordPress backend and the frontend application. They’re independent and can live on different servers – and in most cases, they should. The hosting landscape for headless WordPress has improved significantly – you no longer need expensive managed services for either piece.
WordPress Backend
This is standard PHP/MySQL hosting. Options:
- InstaWP – Spin up WordPress instances in under a minute. Great for development, staging, and production WP backends. The REST API works out of the box with zero config.
- Managed WordPress hosting (Cloudways, Kinsta, WP Engine) – if your WordPress site handles thousands of API requests per minute or has complex plugin requirements.
- Self-hosted – a VPS with PHP and MySQL. More work, more control.
Here’s the thing most guides skip: in a headless setup, the WordPress backend doesn’t serve public traffic. It handles API requests from your frontend and admin traffic from editors. That means you can run a smaller, cheaper hosting plan than a traditional WordPress site needs. A $10/mo managed host is plenty for most headless backends.
Frontend
Your frontend could be a static site, a Node.js server (for SSR), or a mix of both. The hosting requirements depend on your framework and rendering strategy.
| Hosting | Best For | SSR Support | Starting Price | SSH Access |
|---|---|---|---|---|
| Vercel | Next.js (they built it) | Yes (serverless) | Free / $20/mo pro | No |
| Cloudflare Pages | Static sites, Astro | Workers (limited) | Free / $5/mo | No |
| Netlify | Static + serverless functions | Functions only | Free / $19/mo | No |
| InstaPods | Any framework, full Node.js | Yes (full server) | $3/mo flat | Yes |
| Railway | Node.js apps | Yes | $5/mo + usage | No |
| Self-hosted VPS | Full control | Yes | $5-10/mo | Yes |
The right choice depends on your rendering strategy:
- Pure SSG (Astro, static Next.js export) – Any static host works. Cloudflare Pages and Netlify have generous free tiers.
- SSR or ISR (Next.js with server-side features) – You need a Node.js runtime. Vercel, InstaPods, or Railway all work here.
- Full-stack (API routes, auth, database) – You need a real server with SSH access. InstaPods or a VPS.
Deploying the Frontend
Let me walk through three deployment paths for a Next.js headless WordPress frontend.
Option 1: Vercel
The path of least resistance for Next.js. Push to GitHub and Vercel handles the rest.
npm i -g vercel
vercel
Vercel auto-detects Next.js, runs the build, and gives you a URL. SSR runs on serverless functions. ISR works automatically.
Tradeoffs: The free tier is generous for personal projects. But the Pro plan ($20/mo per team member) adds up fast – a 3-person team is $60/mo before you’ve added anything else. Serverless functions have cold starts. And if you need anything beyond Next.js – a background worker, a database, a cron job – you’re looking at additional services.
Option 2: Cloudflare Pages
Best for static exports. If you’re using Astro or exporting Next.js as static HTML, Cloudflare Pages gives you a global CDN with unlimited bandwidth on the free tier.
npm run build
npx wrangler pages deploy out
Tradeoffs: Great for static content. SSR support via Workers exists but has limitations (no full Node.js API, smaller ecosystem). Not ideal if you need ISR or server-side features.
Option 3: InstaPods
For Next.js SSR, ISR, API routes, or any setup where you need a real Node.js server, you can deploy your frontend to InstaPods. You get a full Linux server with SSH access – no serverless limitations, no cold starts.
npm i -g instapods
instapods deploy wp-frontend --preset nodejs
That’s it. The CLI detects your Next.js project, installs dependencies, runs the build, starts the server, and returns a live URL with SSL. The $3/mo plan includes enough resources for a headless WordPress frontend serving thousands of daily visitors – and the pricing is flat, so you won’t get a surprise bill if your blog post goes viral.
You can also SSH in to debug, check logs, or install additional tools:
instapods ssh wp-frontend
When this makes sense: You want ISR or SSR without serverless constraints. You need to run background revalidation. You want SSH access for debugging (try tailing logs on a serverless platform). Or you’re running a frontend that isn’t Next.js and doesn’t have first-class support on Vercel (Nuxt, SvelteKit, custom Node.js).
Common Pitfalls
Headless WordPress has gotchas that aren’t obvious until you hit them in production. Here are the five I see most often.
CORS Errors
If your frontend and WordPress are on different domains (and in a headless setup, they almost always are), you’ll hit CORS errors on client-side API requests. Add this to your WordPress theme’s functions.php:
add_filter('rest_pre_serve_request', function ($served) {
header('Access-Control-Allow-Origin: https://your-frontend-domain.com');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
return $served;
});
This hooks into rest_pre_serve_request so the headers only apply to REST API responses, not every page load. Or use the wp-cors plugin if you don’t want to touch code.
Better approach: If your frontend uses SSG or SSR, API calls happen at build time or on the server – not in the browser. No CORS issues at all. This is another reason to prefer server-side data fetching over client-side fetch calls.
Preview Mode
Content editors expect to click “Preview” in WordPress and see the draft rendered on the frontend. Without extra setup, that button does nothing useful in a headless architecture.
In Next.js, use Draft Mode:
// 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 secret = searchParams.get("secret");
const slug = searchParams.get("slug");
if (secret !== process.env.PREVIEW_SECRET) {
return new Response("Invalid token", { status: 401 });
}
(await draftMode()).enable();
redirect(`/blog/${slug}`);
}
Then in WordPress, set the preview URL to point to your frontend’s preview API route. The WPGraphQL and Faust.js plugins simplify this if you want a turnkey solution.
Revalidation and Stale Content
With ISR, your frontend caches pages and rebuilds them on a timer (the revalidate value). If an editor publishes a post, it won’t appear immediately – it shows up after the revalidation window.
Solutions:
- On-demand revalidation. Set up a WordPress webhook (via the
WP Webhooksplugin or a customsave_posthook) that hits your Next.js revalidation endpoint when content changes:
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
export async function POST(request: Request) {
const { secret, slug } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response("Unauthorized", { status: 401 });
}
revalidatePath(`/blog/${slug}`);
revalidatePath("/blog");
return Response.json({ revalidated: true });
}
- Short revalidation window. Set
revalidate: 10for content that changes frequently. The tradeoff is more API hits to WordPress.
SEO Gaps
This is the one that burns people. The biggest risk with headless WordPress is losing SEO features you didn’t know you had. Yoast, Rank Math, and similar plugins handle canonical URLs, Open Graph tags, XML sitemaps, schema markup, and robots directives automatically in traditional WordPress. In headless mode, none of that frontend rendering happens. You have to rebuild every piece yourself.
Checklist before launch:
- Meta titles and descriptions render correctly (check with
View Source, not browser extensions) - Open Graph and Twitter Card tags are present
- XML sitemap is generated and submitted to Search Console
- Canonical URLs point to the frontend domain, not the WordPress domain
- No
noindextags are accidentally set on the frontend - Internal links point to the frontend URLs, not
wp-jsonendpoints - Structured data (Article schema, BreadcrumbList) is implemented
Authentication for Private Content
If your WordPress site has membership content or private posts, you need to pass authentication tokens with API requests. Use Application Passwords (built into WordPress 5.6+) for server-to-server auth:
const res = await fetch(`${API_URL}/posts?status=draft`, {
headers: {
Authorization: `Basic ${Buffer.from("user:app-password").toString("base64")}`,
},
});
Never expose these credentials in client-side code. Keep auth in server components or API routes only.
Frequently Asked Questions
What is a headless WordPress frontend?
A headless WordPress frontend is a standalone web application – built with React, Astro, Vue, or any other framework – that fetches content from WordPress through the REST API or WPGraphQL. WordPress handles content management and editorial workflow. The frontend handles presentation. The two communicate over API calls instead of WordPress rendering HTML directly through PHP templates.
Is WordPress good as a headless CMS?
Yes. WordPress has a mature REST API (stable since version 4.7), a familiar admin UI, and 60,000+ plugins for content modeling, SEO, and custom fields. It lacks some features purpose-built headless platforms offer (like real-time collaboration or built-in CDN delivery), but for teams already on WordPress, going headless avoids a full CMS migration while still getting modern frontend performance.
Do I need WPGraphQL or is the REST API enough?
The built-in REST API is enough for most headless WordPress projects. It returns JSON, supports filtering and embedding related data, and requires no additional plugins. WPGraphQL is worth adding if you need to fetch deeply nested or highly specific data in fewer requests – it lets you query exactly the fields you need in a single call, which reduces payload size on complex pages.
How do I handle SEO in a headless WordPress setup?
You need to rebuild SEO features that plugins like Yoast handle automatically in traditional WordPress. At minimum: generate meta titles and descriptions via your framework’s metadata API, create an XML sitemap, add Open Graph and Twitter Card tags, implement structured data (Article schema), and ensure canonical URLs point to your frontend domain rather than the WordPress backend. If you use the Yoast REST API extension, you can pull SEO metadata from WordPress and render it on your frontend.
Wrapping Up
Headless WordPress gives you the content management that editors already know with a frontend you want to build. The REST API handles the plumbing. Your framework of choice handles the presentation. And hosting splits cleanly – WordPress on traditional PHP hosting (or InstaWP for quick setups), the frontend on whatever suits your rendering strategy.
The setup is more work than installing a WordPress theme. But the result is a faster, more secure site with a modern development workflow. Once the architecture is in place, the content team keeps using the same WordPress admin they already know – and you get to work in React, Astro, or whatever framework you prefer.
Start with the REST API and a basic Next.js frontend. Get it deployed. Then iterate – add preview mode, on-demand revalidation, and custom fields as you need them. The WordPress REST API has been stable for nearly a decade. The frontend is where you have room to experiment.