The 2026 question is not whether WordPress can run without its theme. It can. The question is which topology survives Gutenberg, preview, login and checkout without turning origin into a public PHP endpoint.
For years headless WordPress meant the editor lost preview, menus became JSON handwork, and every form needed its own REST route. Those holes are closed. WordPress 7.0 (Armstrong) shipped 20 May 2026. WordPress 7.1 on PHP 8.4 is the origin we build against now. Gutenberg delivers block trees as JSON. WPGraphQL has persisted queries. Astro 7 ships HTML with JavaScript only on islands. Next.js 16 ships Partial Prerendering and Server Actions. What remains is architecture: who owns the data contract, where the cache lives, and how the frontend is allowed to talk to origin.
This is an architecture text. The four-year cost model, CapEx and OpEx, lives in the TCO guide for headless versus monolith. Do not paste that table here. Read it when you defend a budget. Read this when you draw boxes and arrows.
Frontend first: Astro 7 islands and Next.js 16
Pick the framework by how much of the page is a document and how much is an application. Content-heavy surfaces belong in Astro 7. Surfaces with session, cart and realtime filters belong in Next.js 16. Both read the same WordPress origin.
Astro 7 compiles pages to HTML. The default first paint is zero kilobytes of JavaScript. Interactive bits (search, filters, mini-cart, login) hydrate as islands with client:visible or client:idle. That matches an editorial WordPress install: most URLs are articles, landings and archives. They do not need a React runtime in <head>.
Next.js 16 is the right call when large parts of the view depend on logged-in state, personalisation, or a cart tree that cannot be prerendered. App Router, React Server Components and Partial Prerendering keep the shell static and fill holes at the edge (price, stock, pay button). Server Actions replace bespoke REST endpoints for forms, as long as the action calls WordPress behind Zero Trust and never exposes an admin cookie to the browser.
A hybrid is common: Astro 7 for magazine, brand and docs, Next.js 16 for My account and checkout. WordPress stays one origin. Two fronts need one shared block-to-component catalogue and one GraphQL schema. Without that catalogue the two views diverge inside a quarter.
Faust.js and similar WordPress-specific Next layers can shortcut preview cookies. They are not a requirement. A thin preview endpoint with HMAC and Draft Mode does the same job, and it works against both Astro and Next. Do not let a vendor starter pick the topology.
Client (browser)
|
v
Edge (Cloudflare): HTML, assets, cache-tags
|
+-- hit: finished document, no PHP
|
+-- miss (preview, My account, checkout)
|
v
Frontend runtime (Astro 7 / Next.js 16)
|
v
Zero Trust gateway
|
v
WordPress 7.1 / PHP 8.4 / WPGraphQL
|
v
MySQL + Redis
If 80 percent of page hits never reach PHP, you have a headless architecture. If every hit reaches origin to “stay dynamic”, you have a more expensive monolith with an extra hop.
WordPress 7.1 as origin, not as the page renderer
This is the international default we ship: WordPress is the content origin, not the HTML factory. The theme directory on origin is either empty of public templates or reduced to a stub that returns 403 for anonymous GET. Editors still live in wp-admin. Visitors never do.
WordPress 7.1 matters here because the block editor already serialises a tree, not a string of PHP-rendered markup. Treat that tree as the contract. Custom post types, taxonomies and ACF fields are first-class. the_content as an HTML bag is a fallback for legacy posts you have not mapped yet, not the target shape.
Two cases we measured before drawing this box:
- A WooCommerce catalogue with 30-plus plugins and TTFB around 1.8 s on category templates. The PHP theme was assembling related products, reviews, schema and a mega-menu on every request. Moving the catalogue to Astro 7 with tagged GraphQL reads dropped first HTML to the edge. Checkout stayed on Next.js 16 because session and payment plugins still needed a request-scoped runtime.
- An Elementor marketing site that collapsed under Black Friday traffic. The page builder assumed PHP would paint every variant. There was no cache-tag story, only “purge all”. Headless did not save that project. We stripped the builder, kept a coupled theme, and put Redis plus page cache in front. Decoupling a page-builder stack is a rewrite of the editorial model, not a hosting change.
Make WordPress Slack (#core-editor, #hosting) and the Block Editor Handbook are the references for the JSON shape, not a third-party “headless WordPress course”. If a block has no save output and only a render.php, it is a PHP-only block. It will not survive decoupling unless you add a JSON renderer.
When decoupling is the right topology
Decoupling pays off when content has more than one consumer, or when security policy forbids exposing PHP to the public internet. It does not pay off as decoration on a three-editor marketing site that ships Gutenberg landings every week.
Choose headless when at least one of these holds:
- The same copy must leave on the web, in an app, on in-store screens and as a source for internal assistants, without the editor pasting it three times.
- The frontend team owns a design system in Astro or React, and WordPress must not dictate markup.
- wp-admin and the database sit on a private network behind SSO. Public origin is static files and cached documents.
- TTFB under 50 ms globally is a requirement, and you accept that the cache lives at the edge with tag invalidation.
Stay on a monolith when:
- Marketing composes landing pages in Gutenberg without waiting on a frontend developer for every new block variant.
- The WooCommerce checkout uses extensions that write HTML, shortcodes and their own session cookies in the same request as the shop.
- The team has PHP capacity and no TypeScript owner. Two pipelines without an owner is not an architecture. It is an incident queue.
A modern monolith on PHP 8.4 with Full Site Editing, Redis and page cache is not “old WordPress”. It loses on multi-channel and on attack surface. It wins on editorial speed. Pick the architecture against the loss you actually have, not against a talk titled headless.
Price and ownership over four years is a different question. Use the TCO guide when you compare year-one CapEx with later operations. This article stops at topology.
Data contract: WPGraphQL, REST and persisted queries
WPGraphQL is the read path for rich pages. REST is the write path for simple events. The mix is deliberate, not a compromise you “clean up later”.
A classic REST call to /wp-json/wp/v2/posts returns dozens of fields an archive does not use. A page that needs the post, the author, three related posts and an ACF repeater becomes several HTTP rounds. WPGraphQL resolves the tree in one call, with DataLoader collapsing ID lookups into WHERE ID IN (...). That is why GraphQL won on decoupled WordPress surfaces, not because GraphQL is fashionable.
Three rules for the schema:
- Expose what the editor owns, not what the theme owned. Custom post types, taxonomies and fields. Not
the_contentas an HTML bag full of shortcodes that expect a PHP runtime. - Persisted queries in production. The client sends a hash, not an arbitrary query. Introspection is off. Max depth and complexity are set. Without that, GraphQL is an open window into the database.
- Cache tags on the response.
post-1425,tax-topic-headless,author-8,lang-en. Without tags you flush the whole edge every time someone fixes a typo.
REST keeps the jobs GraphQL is bad at: inbound webhooks, payment events, form posts from a thin endpoint, and CRUD from internal tools. Those endpoints authenticate with App Passwords or short JWTs, never with an infinite application password sitting in a frontend repo.
<?php
declare(strict_types=1);
add_action('graphql_register_types', static function (): void {
register_graphql_field('Post', 'heroKicker', [
'type' => 'String',
'description' => 'Short kicker above the hero, owned by editorial.',
'resolve' => static fn ($post) => get_post_meta($post->databaseId, 'hero_kicker', true) ?: null,
]);
});
If a field cannot be described in one sentence for an editor, it does not belong on the public schema.
Gutenberg as JSON, not as an HTML bag
The 2026 default is block-to-component mapping. WordPress serialises each block with name, attributes and innerBlocks. The frontend has a dictionary: core/paragraph becomes <Prose>, core/image becomes <Figure>, acf/pricing-table becomes <PricingTable>. Unknown block names render a visible fallback in staging and an empty node in production, with a log line. Silent drop is how design drift starts.
Do not send the_content and hope React will “just render the HTML”. Shortcodes, do_shortcode, Gravity Forms markup and Elementor widgets assume PHP. They will look correct in wp-admin preview against the old theme and wrong on the Astro site. That is the two-week editor-trust failure from the intro, just wearing a different hat.
Keep one catalogue. If magazine is Astro 7 and checkout is Next.js 16, both import the same mapping module. A new block that only exists in one frontend is a product bug, not a nice-to-have.
The Block Editor Handbook and trac tickets around parse_blocks / serialize_blocks are the source of truth for the tree. Vendor “Gutenberg to React” kits go stale the moment core adds a block attribute.
Preview without breaking the production cache
Preview is a feature with a sprint budget. It is not a checkbox on the hosting panel.
Flow we ship:
- Editor clicks Preview in
wp-admin. - WordPress signs a short-lived HMAC token bound to post ID, user ID and expiry.
- The browser opens the frontend
/previewroute with that token. - The frontend runtime bypasses the anonymous cache, fetches the draft over WPGraphQL with the token, and renders the same component tree as production.
- The production cache for the live URL is not touched.
If preview fetches the live permalink instead of the draft, editors will “fix” copy that is already published. If preview shares the anonymous cache key, a draft leaks to the next visitor. Both bugs are worse than a missing preview.
Draft Mode in Next.js and a no-store Astro preview route are implementation details. The contract is: signed, time-limited, post-scoped, and isolated from the public cache key.
Cache tags at the edge
The edge is the HTML store. Origin is the invalidation source.
On publish, update or delete, WordPress fires a webhook with the tags that changed. Cloudflare (or the equivalent) purges those tags. A typo on post 1425 purges post-1425 and maybe tax-news. It does not purge the homepage of every locale.
Stale-while-revalidate is allowed for anonymous documents. It is not allowed for preview, cart, or My account. Those routes are cache-bypass by construction.
We do not host the frontend “on Vercel or Netlify” as a brand choice. We put HTML on Cloudflare because cache tags and HTML are the same product. A platform that can only purge by URL or by “everything” will push you back to full rebuilds. Full rebuilds are how headless projects become slower than the PHP theme they replaced.
For the performance numbers on a shop specifically, see headless WooCommerce on Astro. This article stays on the invalidation model.
Authentication behind origin
Public origin does not speak admin cookies. wp-admin sits behind Zero Trust (Cloudflare Access, or equivalent SSO). Application passwords for the frontend live in the worker or the server, not in a public env file that ships to the browser.
JWT plugins with infinite lifetime are a footgun. If you use JWT, rotate, bind to audience, and keep the private key off the Astro bundle. App Passwords scoped to a bot user (headless-read) are simpler for read-only GraphQL.
Rate limits belong on the gateway, not in a mu-plugin that every request pays for. The gateway sees GraphQL hashes. The mu-plugin sees PHP. Prefer the gateway.
Observability through the chain
A headless miss is three systems: edge, frontend runtime, WordPress. Without trace context you will debug the wrong one.
Propagate W3C traceparent from the edge through the frontend fetch to WPGraphQL. Log the cache-tag set, the persisted-query hash, and the origin status. When LCP regresses, you want to know whether the document was a cache hit, a frontend render, or a PHP miss, not “the site feels slow”.
OpenTelemetry is enough. Do not invent a fourth dashboard.
Contract tests in CI
Every GraphQL schema change is a frontend change. CI must fail if:
- a persisted query hash is missing
- a block name in fixtures has no component
- a required field went nullable
- introspection is on in the production config
We run those checks on pull request, against a staging origin that restored a sanitised production dump. A schema that only exists in a developer’s localwp is not a contract.
What WPPoland builds
We run headless WordPress the same way we run headless WooCommerce: the CMS stays the origin, the frontend ships separately, and preview is a first-class feature.
In practice:
- A REST or WPGraphQL contract documented before the frontend starts, so editors know which fields, taxonomies and block schemas are wired through.
- Preview URLs that sign a draft token in WP and resolve through the frontend draft route, including ACF and Gutenberg block content.
- A redirect map produced from the existing site before launch, so legacy URLs land on the new structure and Search Console does not show a regression spike.
- A rollback plan: the PHP theme stays deployable until the new frontend has held production traffic for at least one full publishing cycle.
If discovery turns up small editorial volume, a plugin stack that depends on the PHP frontend, or no JS engineering capacity, we say so and recommend staying coupled. Headless is a tool, not a status symbol. The commercial surface is the headless WordPress service. The budget conversation is the TCO guide.
For a coupled-to-headless cutover, start with website migration to Astro and Next.js or the Astro development page. The spike is one week: three highest-traffic templates, one draft post with preview, one redirect row. The friction you hit that week is the friction you would hit for a year.
Conclusion
Decouple when content has more than one consumer, when PHP must not be public, or when the frontend team already owns the design system. Stay coupled when Gutenberg is the landing-page factory and checkout still writes HTML in PHP.
WordPress 7.1 on PHP 8.4 is a strong origin. Astro 7 and Next.js 16 are strong fronts. The architecture is the contract between them: persisted queries, block mapping, HMAC preview, cache tags, Zero Trust. Miss one of those and you have not built headless. You have built a slower monolith with extra hops.
If the spike week is clean (three templates, one draft with preview, one redirect row) you have evidence, not a slide. If it is not clean, stay coupled and spend the money on Redis, a page cache, and deleting plugins that paint HTML on every request. That is still architecture. It is just honest about the team you have.





