NetcaneTechnologies

WordPress

Headless WordPress + Next.js: Architecture and Pitfalls

WordPress as a content API and Next.js as the renderer works well — provided you handle revalidation, preview, and block content deliberately. The architecture decisions and the specific pitfalls that catch teams making this move for the first time.

Yasir Haleem6 min read

Headless WordPress is a specific, narrower decision than "should we go headless" — it means keeping WordPress's editorial experience (which teams often like) while replacing its rendering with Next.js (for the performance and design control a theme can't offer). It's a good fit when the editorial team is attached to WordPress's admin and won't move to a different CMS, but the frontend needs to escape theme and plugin constraints. The architecture is well-trodden at this point; the pitfalls that catch teams are specific and repeatable enough to name directly.

API choice: REST is built in, WPGraphQL is more precise

WordPress's REST API ships with core — no plugin required, /wp-json/wp/v2/posts works out of the box. This is the lower-friction starting point, and it's genuinely sufficient for most marketing sites and blogs.

WPGraphQL (a plugin, not core) gives you one endpoint and the ability to request exactly the fields and relations you need in a single query — useful once you're assembling several related pieces of content per page (a post, its author, related posts, and site-wide menu data, say) and want to avoid several sequential REST calls to get there.

Whichever you choose, centralize every API call in one data layer (lib/wp.ts or similar) rather than scattering fetch calls across page components. This is what makes it survivable when WordPress or a plugin changes its response shape — you have one place to update, and ideally typed responses so a breaking change fails a type check instead of silently rendering undefined in production.

// lib/wp.ts
const WP_URL = process.env.WP_URL;
 
export async function getPost(slug: string) {
  const res = await fetch(
    `${WP_URL}/wp-json/wp/v2/posts?slug=${slug}&_embed`,
    { next: { revalidate: 60 } },
  );
  const data = await res.json();
  return Array.isArray(data) ? data[0] ?? null : null;
}

Data fetching and caching: revalidation is not optional

Fetch content in Next.js Server Components (or getStaticProps/ISR patterns on older Next versions), and use time-based revalidation (next: { revalidate: 60 }) as a baseline so content doesn't require a full rebuild to update.

The pitfall here is specific and common: publish something in WordPress and it doesn't show up on the frontend for up to the full revalidation window — fine for a blog post where a minute of staleness is a non-issue, not fine for a client who just corrected a pricing page and expects it live immediately. Wire a webhook from WordPress on publish/update (a plugin like WP Webhooks, or a small custom action hook) to trigger revalidatePath or revalidateTag in Next.js directly, rather than relying purely on the time-based window. Time-based revalidation is the safety net; event-based revalidation is what makes "I just published this" actually true.

Preview and drafts: server-side only, no exceptions

Previewing an unpublished post requires authenticated access to WordPress — an application password or a cookie-based session — and that authentication must never reach the browser. Use Next.js's draftMode() API: a preview route sets the draft-mode cookie and redirects to the post; your data layer checks draft mode server-side and, only then, requests the draft version from WordPress with server-held credentials.

The mistake to avoid explicitly: never expose WordPress admin credentials, application passwords, or an API token in client-side code — not in a NEXT_PUBLIC_ variable, not in a client component, not "just for testing." If it's reachable from the browser, it's public, and a leaked WordPress application password is a real compromise vector, not just an inconvenience.

Block content: parse it deliberately, don't just dump HTML

WordPress's block editor (Gutenberg) stores content as block markup, and the REST/GraphQL response typically includes rendered HTML for the content field. Rendering that HTML directly (dangerouslySetInnerHTML in React) works as a starting point but has real costs: you lose the ability to apply your own component styling per block type, and you're trusting WordPress's rendered output for whatever plugins happen to be active, including any that inject unwanted markup.

For anything beyond a simple blog, parse the block markup and map block types to your own React components — a custom parser, or a library built for this, gives you a paragraph block that renders as your styled <Paragraph>, an image block that goes through next/image, a custom block type that renders your actual component instead of whatever HTML WordPress generated for it. This is more work up front and it's the difference between a headless WordPress site that looks and performs like a real Next.js app versus one that's secretly still constrained by WordPress's rendered HTML.

Media, menus, and the data that isn't "a post"

Media — proxy or directly reference WordPress media URLs, but route them through next/image with your WordPress domain added to remotePatterns, so images actually get resized and format-converted rather than shipped at whatever resolution WordPress's media library happens to store.

Menus and site-wide options (navigation structure, footer content, global settings) live outside the post/page content types and need their own fetch, typically via REST's menu endpoints (with a plugin, since core doesn't expose these by default) or a WPGraphQL query. Cache this data — it changes rarely — and make sure it's fetched once and available wherever your layout needs it, not re-fetched per page.

The pitfalls, condensed

PitfallWhat happens without the fixFix
No revalidation webhookPublished changes invisible until the next time-based windowWebhook on publish → revalidatePath/revalidateTag
Client-exposed credentialsLeaked application password, real security exposureDraft fetching stays server-side, always
Raw block HTML rendered directlyLocked into WordPress's markup, can't style per blockParse blocks, map to your own components
Unoptimized media URLsFull-resolution WordPress uploads shipped to every devicenext/image with WP domain in remotePatterns
Menus/options fetched ad hoc per pageRedundant requests, inconsistent cachingFetch once, cache, thread through layout

Where this architecture is the right call

Headless WordPress earns its complexity when the editorial team's attachment to WordPress is real and non-negotiable, and the performance/design ceiling of a theme is the actual problem being solved. If neither is true — if the team would happily use a purpose-built headless CMS instead, or if a well-optimized traditional WordPress theme would solve the actual complaint — this architecture adds two systems' worth of maintenance for a benefit that a simpler option might deliver too.

This is one specific variant of the headless CMS implementations we deliver, and the pitfalls above are exactly what we build against from day one — revalidation wired to publish events, preview kept server-side, block content parsed into real components. If you're running (or planning) a headless WordPress build and want a second opinion on the architecture, get in touch.

About the author

Yasir Haleem is founder and lead engineer at Netcane Technologies. He builds production Next.js sites with headless CMS platforms — Strapi, Contentful, Sanity, and WordPress — with a focus on performance, SEO, and maintainable architecture.

Let's work together

Tell us about your project. We respond within one business day with a clear scope, timeline, and estimate.