ARCHITECTURE
Headless WordPress with a Laravel API
Decoupling gets sold as a performance fix. It is really an editorial trade. You buy a faster front end with a slower publishing loop, and only some businesses should take that deal.
Going headless means WordPress stops rendering your pages. It keeps the admin, the editorial workflow and the content, while something else (Astro, Next, a Laravel app) reads that content over an API and renders the front end.
It is the right call less often than the conference talks suggest, and we talk more people out of it than into it. That is not false modesty. Roughly two thirds of the performance gain usually available on a slow WordPress site comes from work that does not require decoupling at all.
Here is how we decide, what it costs, and how to get most of the benefit without the architecture.
The case where it earns its keep
The shape that justifies it: a publisher or a large content site where page speed is tied to revenue, a theme that has accreted a decade of plugins each adding a script to the head, and a homepage making dozens of requests before a reader sees anything.
The constraint that decides the architecture is usually editorial rather than technical. A dozen editors who know the WordPress admin, publish several times a day, and are not going to learn a new system. Any solution that changes their day is not a solution, which rules out replatforming and leaves decoupling as the only route to a fast front end.
Done properly, that means the WordPress admin stays exactly as they know it, rendering moves to a static front end against an API, and the image pipeline and caching get rebuilt on the way past. Mobile LCP in the low seconds becoming mobile LCP around a second is a realistic target. Editorial workflow unchanged is the acceptance criterion.
Budget a quarter and a small pod.
What decoupling actually changes
Worth being precise, because the word covers three separate changes and people usually want only one of them.
Rendering moves. HTML is produced by something other than a WordPress theme. This is the change that buys you the performance, and it is also the change that takes the visual tooling away from your editors.
Delivery changes. Pre-rendered pages come from a CDN rather than from PHP. This is where the reliability improvement lives: a traffic spike that would have taken the origin down now hits static files.
The content becomes an API. Which means other things can consume it: an app, a partner, a newsletter builder. This is often the real reason a business wants it, and it is worth naming rather than dressing up as performance.
If you want only the second one, page caching in front of a normal WordPress site gets you most of it for a tiny fraction of the cost.
Why Laravel in the middle at all
Most headless WordPress builds talk to WP directly over WPGraphQL or the REST API. That is simpler, cheaper and one fewer thing to run. You should do it if it fits.
We put Laravel between them when one of three things is true.
Content is not the only source. A publisher typically has subscriber data in one system, ad configuration in another and content in WordPress. Something has to compose those into a page, and doing it in the front end means three round trips and three separate failure modes visible to the reader. Composing server side turns that into one call against one cache.
WordPress cannot take the read traffic. WPGraphQL queries are expensive, and
wp_postmeta does not enjoy being a public API. A single article page can fan
out into dozens of meta lookups. Laravel in front, with a real cache and a
normalised read model, absorbs it and means a traffic spike never reaches
WordPress at all.
You need a write path that is not WordPress. Comments, saved articles, paywalled access, reading history. That is application state with real constraints, and it does not belong in postmeta. This is the same boundary argument as choosing between the two platforms in the first place.
If none of those is true, skip Laravel. Astro reading WPGraphQL directly is less machinery and less to maintain, and every layer you run is a layer somebody patches.
What “normalised read model” means in practice
The point of the middle layer is that it stores content in the shape the front end needs, not the shape WordPress stored it in.
// One row per article, written on the publish webhook.
Schema::create('articles', function (Blueprint $t) {
$t->id();
$t->unsignedBigInteger('wp_post_id')->unique();
$t->string('slug')->unique();
$t->string('title');
$t->text('excerpt');
$t->json('blocks'); // already parsed, not raw post_content
$t->json('seo'); // resolved, not computed per request
$t->foreignId('author_id');
$t->timestamp('published_at')->index();
});
Every expensive decision (parsing blocks, resolving ACF fields, computing SEO tags, picking image sizes) happens once, on publish, rather than on every request. That is most of where the read performance comes from, and it is the part a direct WPGraphQL setup does not give you.
What it costs your editors
This is the part that gets skipped in the proposal, and the part that kills projects in month three.
Preview breaks. WordPress previews through the theme. If the theme no longer renders the site, preview shows something that is not your site. You have to build a preview route in the front end and wire the admin to it, including for drafts and scheduled posts. Budget a week and treat it as non-negotiable, because editors who cannot preview will not adopt the system, and an unadopted system gets reverted.
Publishing stops being instant. With a statically rendered front end, publishing triggers a build or a revalidation. Even at ten seconds, an editor used to instant will notice, and a breaking-news desk will hate it. Use incremental revalidation or on-demand rebuilds for affected routes, never full site rebuilds. A full rebuild on a 20,000-article site is a ten-minute wait, and it will be the reason the project is judged a failure.
Anything visual moves out of reach. Widgets, the customiser, block patterns carrying styling, plugins that inject markup: none of it applies to a front end WordPress does not render. Every one the editors used has to be rebuilt as a component or dropped, and “dropped” needs to be a conversation rather than a discovery.
Search has to be rebuilt. WordPress search is a theme feature. Losing it is usually an improvement, since core search is poor, but it is a line item. Typesense or Meilisearch fed from the read model is a few days of work and a much better result.
Forms and comments need a home. Contact forms, newsletter signups and comments all assumed a PHP page was rendering. Each needs an endpoint on the new side, plus spam handling you previously got from a plugin.
Third-party embeds get fussy. Anything that shipped a WordPress shortcode or
relied on wp_head output needs re-implementing. Ad tags, analytics, consent
managers, video players. None individually hard, collectively a fortnight.
We now start these projects by listing what the editors do in a typical week, because that list is the real specification and it is never the same as the brief.
The shape we build
WordPress (admin only)
↓ webhook on publish
Laravel API ← Redis cache, normalised read model
↓ JSON
Astro front end → CDN
A few decisions that have held up across builds.
WordPress is not public. It sits behind auth on a subdomain. That removes an entire category of security problem, keeps the WP install small, and means the plugin count stops being a performance question and becomes only a security one.
Laravel owns the cache, not the front end. One place to invalidate, one place to reason about staleness. A publish webhook busts the affected keys and triggers revalidation of the affected routes.
// The publish webhook: update, invalidate, revalidate. In that order.
public function handle(Request $request): Response
{
$article = $this->importer->syncFromWordPress($request->integer('post_id'));
Cache::tags(['article:' . $article->id, 'index'])->flush();
RevalidateFrontend::dispatch($article->affectedRoutes());
return response()->noContent();
}
The order matters. Revalidating before invalidating means the front end fetches and caches the stale version, and you get to explain why the correction has not appeared.
Content is normalised on the way in, not the way out. ACF fields become typed columns or a documented JSON shape in Laravel. Doing it at read time means every consumer re-implements the same parsing, and the second consumer always does it slightly differently.
Images are processed once, not per request. The image pipeline was half the
LCP win: AVIF and WebP with correct sizes, generated on upload, served from
the CDN. This is also the piece most easily lifted back into a conventional
WordPress site.
Everything is reversible for the first month. The old theme stays deployed and one config change away. It is rarely needed, and it is usually the reason everyone is willing to launch.
Where the performance actually came from
Worth being precise, because “headless is faster” is not really true. A well-built WordPress site with good caching is fast too.
A typical apportionment when a slow WordPress site is taken from several seconds to about one, and this shape is remarkably consistent:
| Change | Share of the saving | Needs headless? |
|---|---|---|
| Deleting render-blocking plugin scripts | ~40% | No |
| Rebuilding the image pipeline | ~30% | No |
| Pre-rendered HTML from a CDN | ~20% | Yes |
| Font strategy and critical CSS | ~10% | No |
Roughly two thirds of the win needs no decoupling at all. That is worth knowing before you commit: if performance is your only motivation, do the cheap two thirds first and see whether you still care.
The cheap two thirds, in order
If you want the 2.3 seconds without the architecture, this is the order we would work in on a conventional WordPress site. Most of it is a two to three week engagement rather than a twelve week one.
- Audit what is in the head. Every plugin adding a render-blocking script or stylesheet, and whether the page it loads on needs it. Conditional dequeuing is unglamorous and it is usually the single biggest win.
- Fix the images. Correct dimensions, modern formats,
loading="lazy"on everything below the fold and explicitly not on the LCP image, realsizesattributes. Generate on upload rather than on request. - Page caching, properly. Full-page cache with a sensible invalidation strategy, served before PHP runs. Plus a persistent object cache in Redis.
- Fonts. Self-hosted, preloaded,
font-display: swap, subset to the characters you use. Third-party font hosts cost a connection you cannot afford in the critical path. - Critical CSS for the templates that matter. Two or three templates cover most traffic on most sites.
Measure after each step. If you are at 1.8 seconds by step three, stop, because the remaining second costs more than the four before it did.
What it costs to build and to run
The publisher project was twelve weeks with a pod of four. At our published rates that is a six-figure engagement, and it was justified by advertising revenue tied directly to page speed. Most sites do not have that arithmetic.
Running costs change too, in both directions:
| Conventional WordPress | Headless with Laravel | |
|---|---|---|
| Hosting | One managed host | WP host, Laravel host, CDN, Redis |
| Things to patch | WordPress and plugins | WordPress, Laravel, front end deps |
| People needed | WordPress developer | WordPress, PHP and front end |
| Origin load under a spike | Scales with traffic | Flat |
The last row is the one that pays. The three above it are the ones that get forgotten in the business case.
When we say no
Small content sites. A block theme, a good host and disciplined plugins give you a fast site for a fraction of this. Headless is not a performance strategy for a 40-page site, it is over-engineering with a maintenance bill.
Teams without a front-end developer. You are taking on a JavaScript application. If nobody on your side can maintain it, you have swapped a problem you understand for one you do not, and the first dependency upgrade will prove it.
Editorial teams who publish constantly and hate friction. A newsroom publishing forty times a day is the worst possible fit for a build step, and no amount of incremental revalidation makes it feel like the classic setup.
Anyone whose actual problem is the content model. If editors fight the site because the fields are wrong, decoupling changes nothing. Fix the content model. That is cheaper, faster, and it is usually the real complaint underneath the stated one.
Anyone who has not done the cheap two thirds. We will not quote a headless build for a site that still loads six render-blocking plugin scripts. Do that work first. If you are still unhappy afterwards, the case for decoupling is now a real one and we can have a proper conversation about it.
If you do it
- Build the preview route in the first fortnight, not the last.
- Write down the editors’ weekly tasks before design starts, then check them off at launch. That list is the acceptance criteria.
- Keep the WordPress install boring: fewer plugins, no theme work, admin only, behind auth.
- Put a real cache in front of the API from day one. Retrofitting invalidation is unpleasant and it is where the subtle bugs live.
- Decide the invalidation order once, write it down, and make everyone follow it. Invalidate, then revalidate.
- Measure before and after on real devices over real networks, not on a laptop on office wifi. The number that matters is field data, not a lab score.
- Keep the old theme deployable for a month.
Done well it is a genuinely good architecture and the teams running it do not want to go back. Done because it was on a conference slide, it is a maintenance burden with a faster homepage.
Common questions
Does headless help SEO?
Indirectly, through Core Web Vitals and through server-rendered HTML being reliably crawlable. It also introduces new ways to break SEO: canonical tags, structured data and redirects all have to be re-implemented deliberately, because the plugins that handled them no longer render anything. We have audited headless builds that lost rankings for exactly that reason.
Can we keep Yoast or Rank Math?
You can keep them for the editorial interface, where they are genuinely useful, and read their stored fields through the API. What you cannot keep is their output, since they produce markup for a theme that no longer exists. Resolve their fields into your read model at publish time and render them yourself.
Is Astro better than Next for this?
For a content site, usually. It ships less JavaScript by default, which is most of the point. Next is the better answer when the front end is genuinely an application with a lot of client-side state. Both work; the decision should follow how interactive the front end is, not which is more fashionable.
How do redirects work?
They move to the front end or the CDN, and they need migrating deliberately. A WordPress redirect plugin holds hundreds of rules nobody has looked at in years. Export them, review them, and put them where the new front door is. Losing them silently is one of the two or three most common ways these launches go wrong.
Can we go back if we hate it?
Yes, if you keep the theme deployable and WordPress remains the source of truth, which in this architecture it does. That is a large part of why we keep the old theme for a month after launch. The reverse migration is a config change rather than a project, provided nobody started writing content into the middle layer.