Headless WordPress with Next.js: A Developer’s Overview

Headless WordPress with NextJS: A Developer’s Overview
Of all the ways to build headless WordPress, the Next.js pairing is the most common in production — and for good reason. Next.js’s rendering modes map almost perfectly onto the problems headless WordPress sites have: mostly-static content that occasionally changes, SEO requirements that rule out client-only rendering, and preview workflows editors expect. This is a developer’s overview of how the stack fits together: the API layer, data fetching patterns, rendering trade-offs, and the gotchas we’ve learned shipping these builds.
This isn’t a step-by-step tutorial — it’s the architectural picture you need before writing the tutorial-following code. (For the “should you?” decision, see our headless WordPress examples.)
The Stack at a Glance
A typical headless wordpress with nextjs build has three layers:
- WordPress backend — content management, user roles, media library. Usually on a private or restricted host; the public never touches it directly. WPGraphQL (or REST API) exposes content.
- Next.js front end — fetches content at build/request time, renders pages, handles routing, forms, and interactivity. Deployed on Vercel, Netlify, or a Node host.
- The contract between them — the GraphQL schema (or REST response shape). Treat this as an API contract: versioned, documented, and stable.

Choosing the API: WPGraphQL vs REST
This is the first real decision, and it shapes everything downstream.
WPGraphQL (recommended for most builds)
WPGraphQL exposes your entire WordPress content model — posts, pages, custom post types, taxonomies, menus, ACF fields — through a single /graphql endpoint. The front end requests exactly the fields it needs:
query GetPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
excerpt
date
content
featuredImage {
node {
sourceUrl
altText
}
}
author {
node {
name
}
}
}
}
One request, exactly the data the template needs, nothing more. As the content model grows — custom post types for case studies, team members, locations — GraphQL scales gracefully because each template declares its own data requirements. TypeScript codegen (via graphql-codegen) turns the schema into types, so your front end gets compile-time safety against the content model.
Use WPGraphQL when: the content model has more than posts and pages, multiple templates need different data shapes, or you want TypeScript safety across the API boundary.
REST API (fine for simple builds)
WordPress core’s REST API needs no plugins and works immediately:
// Fetching posts via the REST API
const res = await fetch(
`${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/posts?slug=hello-world&_embed`
);
const posts = await res.json();
The _embed parameter pulls in author, featured media, and terms in one go — without it, you’ll make several follow-up requests per post, which gets slow and ugly fast. The REST API is simpler to reason about and easier to debug (it’s just URLs), but response shapes are fixed and often include fields you don’t need.
Use REST when: the site is a straightforward blog or marketing site, you want zero plugin dependencies on the WordPress side, or the team is more comfortable with REST than GraphQL.
Either way, one non-negotiable: create a dedicated API user with minimal capabilities and authenticate server-side. Never expose admin credentials to the client, and never call the WordPress API directly from the browser in production — route everything through Next.js server components, route handlers, or getStaticProps-era data functions so credentials stay server-side.
Data Fetching Patterns for Headless WordPress with NextJS
How you fetch is as important as what you fetch. Next.js (App Router) gives you several patterns; here’s how each maps to headless WordPress content.
Static generation (the default for content)
Blog posts, marketing pages, case studies — content that changes infrequently should be rendered at build time into static HTML. In the App Router, fetch calls in Server Components are statically rendered by default:
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(process.env.WORDPRESS_GRAPHQL_ENDPOINT!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
}
}
`,
variables: { slug },
}),
// Cache the response; revalidate on a schedule (see ISR below)
next: { revalidate: 3600 },
});
const { data } = await res.json();
return data.post;
}
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
{/* WordPress content is HTML — sanitize or trust your editors */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Two things to note in that snippet. First, dangerouslySetInnerHTML is doing exactly what it says — WordPress content is HTML, and rendering it means trusting it. If untrusted users can publish (contributors, user-submitted content), sanitize server-side with something like isomorphic-dompurify. Second, generateStaticParams should enumerate your slugs at build time so pages exist as static files rather than rendering on first request:
export async function generateStaticParams() {
const posts = await getAllPostSlugs(); // lightweight query: slugs only
return posts.map((post) => ({ slug: post.slug }));
}
ISR: Incremental Static Regeneration
Static generation has an obvious problem: when an editor publishes a post, the site doesn’t know until the next build. ISR solves this — pages are static, but Next.js regenerates them in the background after a revalidation interval or on demand.
Time-based revalidation (revalidate: 3600) is the simple version: pages refresh at most once per hour. Fine for blogs.
On-demand revalidation is the professional version: WordPress fires a webhook on publish/update, hitting a Next.js API route that revalidates exactly the affected paths:
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
// Verify the webhook secret — never revalidate on unauthenticated requests
const { searchParams } = new URL(req.url);
if (searchParams.get("secret") !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: "Invalid token" }, { status: 401 });
}
const { slug, type } = await req.json();
// Revalidate the specific post and any listing pages that include it
revalidatePath(`/blog/${slug}`);
revalidatePath("/blog");
return NextResponse.json({ revalidated: true });
}
On the WordPress side, hook into save_post to POST to this endpoint. The result: editors publish in WordPress, and the change is live on the static front end within seconds — the headless editorial experience done right.
SSR: Server-Side Rendering
Some pages can’t be static: personalized content, geo-targeted pages, real-time inventory, authenticated views. For these, fetch at request time on the server (cache: 'no-store' or dynamic rendering). SSR keeps SEO intact (the HTML is fully rendered for crawlers) while serving fresh data.
The cost is latency and server load — every request hits WordPress’s API. Use SSR surgically, only for pages that genuinely need per-request data. A common mistake is rendering the entire site with SSR “for freshness” and ending up slower than the traditional WordPress site it replaced.
Client-side fetching (use sparingly)
Search-as-you-type, filters, infinite scroll, “load more” — these legitimately fetch client-side. Route them through Next.js Route Handlers (which can add authentication and caching) rather than calling WordPress directly from the browser. Direct browser-to-WordPress calls leak your API structure and bypass any caching layer.
ISR vs SSR: The Trade-off, Plainly
| ISR (static + revalidation) | SSR (per-request) | |
|---|---|---|
| Time to first byte | Excellent (CDN-served) | Good (server render + API call) |
| Freshness | Seconds to minutes behind (with on-demand revalidation) | Always current |
| Scales under traffic | Effortlessly (it’s static files) | Limited by WordPress API throughput |
| Best for | Posts, pages, listings, marketing content | Personalized, authenticated, real-time data |
| Failure mode | Stale content if webhooks break | Slow/error pages if WordPress API is down |
The winning architecture for most headless WordPress sites: ISR for ~95% of pages, SSR for the dynamic 5%. If your build is 100% SSR, you’ve built a slower, more expensive traditional WordPress site with extra steps.
Handling WordPress-Specific Concerns in Next.js
Menus and navigation
WordPress menus come through WPGraphQL cleanly. Fetch them once and cache aggressively — navigation changes rarely, and every page needs it:
query GetMenu {
menu(id: "primary", idType: LOCATION) {
menuItems {
nodes {
label
uri
childItems {
nodes {
label
uri
}
}
}
}
}
}
Cache menu data with a long revalidation window or a dedicated webhook. Don’t fetch navigation per-page at request time.
SEO: meta tags, sitemaps, redirects
This is where headless builds most often regress versus traditional WordPress, because plugins did it all automatically. Your Next.js app must deliberately implement:
- Meta tags and Open Graph: Use Next.js Metadata API per page, populated from SEO fields. If you use Rank Math or Yoast, their data is exposed via WPGraphQL extensions — or store SEO fields in ACF and query them directly. Don’t skip this; “we’ll add SEO later” is how headless sites launch invisible to Google.
- XML sitemaps: Generate at build/request time from the WordPress API (
app/sitemap.ts). Include posts, pages, and custom post types. - Redirects: Export WordPress redirects (Redirection plugin data or Rank Math’s) and implement them in
next.config.jsor middleware. Forgetting redirects during a headless migration is a classic traffic-killer. - Schema markup: Emit JSON-LD per template (Article schema for posts, etc.). You own this now — there’s no plugin doing it for you.
Images
WordPress generates multiple image sizes, but a headless front end should use Next.js <Image> with a custom loader pointing at WordPress’s image URLs (or better, a CDN/image service in front of them). Serve modern formats, lazy-load below the fold, and set explicit dimensions to avoid layout shift. Media-heavy headless sites live or die on image strategy.
Preview and draft mode
Editors need to preview unpublished content. The pattern: WordPress preview links hit a Next.js API route that validates a token, enables Draft Mode, and renders the draft content fetched with authentication:
// app/api/preview/route.ts — simplified
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get("secret");
const slug = searchParams.get("slug");
if (secret !== process.env.PREVIEW_SECRET || !slug) {
return new Response("Invalid token", { status: 401 });
}
draftMode().enable();
redirect(`/blog/${slug}`);
}
When draft mode is on, your data functions fetch with preview credentials and skip caching. This is genuinely important — a headless build without working preview will erode editor trust within weeks.
Forms
Contact forms, newsletter signups, quote requests — none of the WordPress form plugins’ front-end output works headless. Build forms as React components and POST to Next.js Route Handlers, which validate, sanitize, and forward to your CRM, email service, or back into WordPress (via REST API or a plugin endpoint). This is actually an improvement over plugin forms in most cases: better validation, better UX, no plugin shortcode soup.
Faust.js: Worth Knowing About
Faust.js (from WP Engine) is a framework specifically for headless WordPress with Next.js — it handles authentication, previews, sitemaps, and template hierarchy mapping so you don’t reinvent them. If your team is new to headless WordPress, Faust removes a meaningful chunk of boilerplate and encodes patterns the community has settled on. If your team is experienced, you may prefer the control of a hand-rolled setup. Either is defensible; what’s not defensible is hand-rolling auth and preview badly when a tested solution exists.
Deployment Notes
- Environment separation: The Next.js app needs
WORDPRESS_API_URL(server-only) and preview/revalidation secrets. Never prefix WordPress credentials withNEXT_PUBLIC_. - Build resilience: If WordPress is unreachable during build, the build fails. For large sites, consider building critical pages statically and letting the long tail render on first request (with ISR caching it after).
- Monitoring both sides: Uptime monitoring on the front end alone misses WordPress API outages that break revalidation and SSR pages. Monitor both, and alert on webhook failures.
- Rate limiting the API: A misconfigured front end can DDoS your own WordPress backend (uncached SSR pages + traffic spike). Cache aggressively, and consider a CDN or object cache in front of the WordPress API.
our headless WordPress development
The Honest Summary
Headless WordPress with Next.js is the best-documented, best-tooled headless WordPress path in 2026: WPGraphQL gives you a typed content API, Next.js gives you rendering modes that match content’s actual freshness needs, and the ecosystem (Faust.js, Vercel’s WordPress tooling) removes much of the boilerplate. The architecture earns its complexity when performance, multi-platform publishing, or front-end freedom are genuine requirements.
It does not earn its complexity as a default. If you’re reaching for this stack, be able to finish the sentence: “We’re going headless because ___.” If the blank is “it’s modern,” build a block theme instead — your future self will thank you.
If your blank has a real reason in it, get in touch — this is architecture we work with regularly, and we’ll scope it honestly.
FAQ
Is Next.js good for headless WordPress?
Yes — it’s the most widely used front end for headless WordPress. Static generation and ISR match how content sites actually behave (mostly static, occasionally updated), the Metadata API handles SEO properly, and the ecosystem including Faust.js provides WordPress-specific tooling.
Should I use WPGraphQL or the REST API with Next.js?
WPGraphQL for anything beyond a simple blog — it gives precise data fetching, handles complex content models well, and supports TypeScript codegen. The REST API is fine for simple sites and needs no extra plugins. Both work; the choice depends on content complexity.
What is ISR in headless WordPress?
Incremental Static Regeneration lets Next.js serve pages as static files while updating them in the background — either on a timer or on demand via webhooks from WordPress when content is published. Editors get near-instant publishing; visitors get static-file speed.
How do WordPress previews work in a headless Next.js site?
Through Next.js Draft Mode: WordPress preview links hit an API route that validates a secret token, enables draft mode, and renders the unpublished content fetched with authenticated API requests. It requires deliberate setup — it doesn’t work out of the box.
Can I use WordPress plugins with headless Next.js?
Backend plugins (SEO metadata, custom fields, security, backups) work normally. Plugins that render front-end output — page builders, most form plugins, sliders — don’t; their functionality must be rebuilt in Next.js. This is the main trade-off of the architecture.
How much slower is development for headless vs traditional WordPress?
Expect a headless Next.js build to take 1.5x–3x the effort of a comparable traditional theme build, largely because SEO features, forms, previews, and redirects that plugins provide for free must be deliberately implemented. Ongoing maintenance is also higher with two systems to operate.



