headless-wordpress-astro-setup.html
< BACK TO BLOG Vintage desk with manuscript pages and tape machine in warm golden-hour light

Headless WordPress + Astro: A Working Setup for Content-Heavy Sites

A client rang me in early 2023 — a media publisher running about 14,000 posts on WordPress, a theme cobbled together across four developers over six years, and a Core Web Vitals score that was genuinely embarrassing. Their LCP was clocking in at 7.2 seconds on mobile. They'd tried WP Rocket, they'd tried a CDN, they'd even stripped half their plugins. Still slow. The problem wasn't WordPress itself. It was that every single page render was going through PHP, a bloated theme, and a database query chain that hadn't been looked at since 2018.

That's the moment I properly committed to a headless setup with Astro as the frontend. Not because it's fashionable — because for a site pushing thousands of posts with heavy editorial content, it was the only architecture that made sense.

Here's exactly how I set it up. The tradeoffs, the real config, the bits that bit me.

---

Why Astro and Not Next.js

I get this question a lot. Next.js is the obvious answer if you've already got a React-heavy team or you need complex client-side interactivity. But for content-heavy sites? Astro wins, and it's not particularly close.

Astro ships zero JavaScript by default. For a blog, a news site, or a documentation portal, that's the correct default. You opt into JavaScript where you need it, rather than opting out of a 200kb React bundle you mostly don't want. The Astro docs on partial hydration — they call it the Islands architecture — explain it better than I can in a sentence, but the short version is: only the interactive bits get JS. The article body, the header, the sidebar? Static HTML.

I built a legal content site in late 2022 with Next.js and WordPress. Fast enough, but the client kept asking why their Lighthouse score was 74 on mobile when "it's supposed to be fast now". Hydration overhead. With Astro, that same type of site now routinely hits 95–98. Not bragging — that's just what the architecture gives you for free.

What Astro Is Not Good At

Worth being honest. If your site needs real-time personalisation, a heavy shopping cart, or anything that really depends on client-side state across many components, Astro starts to feel awkward. It's not a React app. The Islands pattern is powerful, but it's a different mental model than building SPAs. I tried shoehorning a client dashboard into an Astro project in mid-2023 and ended up reverting to Next.js within two weeks. Know what you're building.

---

Setting Up WordPress as a Headless CMS

WordPress is a genuinely good headless backend. The WP REST API ships in core, it's well-documented, and your editorial team doesn't have to learn anything new. That last point matters more than developers usually admit.

Here's the setup I use:

  1. Install WordPress on a subdomain — I use cms.yourdomain.com or api.yourdomain.com. Keep it behind basic auth or at minimum restrict direct public traffic. The frontend is yourdomain.com. Two separate deployments.
  2. Install the [WPGraphQL plugin](https://www.wpgraphql.com/) — I prefer GraphQL over REST for content sites because you can co-locate your queries with your components and fetch exactly the fields you need. No over-fetching. The REST API is fine, but once you've got 15+ custom fields per post type, the GraphQL approach is noticeably cleaner.
  3. Install Advanced Custom Fields (ACF) and the WPGraphQL for ACF extension. This combo is what makes WordPress genuinely flexible as a headless content model — you can define structured data per post type, expose it through GraphQL, and Astro consumes it cleanly.
  4. Disable comments, emojis, and the default XML-RPC if you haven't already. These add overhead and attack surface you don't need.
  5. Set permalinks to something sensible before you start building. Changing them mid-project when your Astro routes are already set is a genuine pain.

One thing that trips people up: CORS. By default, WordPress won't let your Astro dev server (running on localhost:4321) make requests to your WP install. Drop this into your theme's functions.php or a small utility plugin during development:

`` add_action('init', function() { header("Access-Control-Allow-Origin: *"); }); ``

Tighten that to specific origins in production. Obviously.

---

The Astro Project Structure

I keep this opinionated and consistent across projects. After a dozen or so headless builds, this is what works:

`` src/ components/ layouts/ pages/ index.astro blog/ [slug].astro lib/ wpgraphql.ts ← all WP query logic lives here styles/ ``

The lib/wpgraphql.ts file is where I centralise every GraphQL fetch. No inline fetch calls scattered across page files. Every query is a named, exported async function. Debugging this across 14,000 posts when something breaks at 2am — you'll thank yourself later.

Fetching Posts at Build Time

Astro's getStaticPaths is your bread and butter here. For a blog with thousands of posts:

`` export async function getStaticPaths() { const posts = await getAllPostSlugs(); // calls WPGraphQL return posts.map(post => ({ params: { slug: post.slug }, })); } ``

getAllPostSlugs paginates through WPGraphQL using after cursors — WordPress's GraphQL layer returns 100 posts per request by default, so for 14,000 posts you're making 140 requests at build time. That sounds scary. In practice, on a decent server, the full build runs in about 4–5 minutes. Perfectly acceptable for a site that rebuilds a few times per day.

---

Handling Images Without Losing Your Mind

This is the bit nobody talks about enough. WordPress stores image URLs pointing to your CMS subdomain. When Astro builds statically, those images still live on cms.yourdomain.com — which means your visitors' browsers are fetching images from your WordPress server, potentially bypassing your CDN.

A few ways I handle this:

  • Cloudflare in front of both domains. Simplest option. Proxy both yourdomain.com and cms.yourdomain.com through Cloudflare, configure aggressive caching on /wp-content/uploads/*, and you're mostly fine.
  • Use a media offload plugin. I like WP Offload Media — it moves uploads to S3 (or compatible storage) and rewrites URLs automatically. This is the approach I use for any site expecting serious traffic. Your WordPress server stops serving images entirely.
  • Astro's Image component. For images you control at build time (featured images pulled via GraphQL), you can pass the remote URL into Astro's <Image> component and it'll optimise, resize, and serve them from your build output. Works brilliantly. Does not work for images embedded in post body HTML — that requires a different pass.

Seahawk had a travel content client last year — about 8,000 posts, extremely image-heavy, average 12 images per article. Their WordPress server was getting hammered purely by image requests even with the headless setup. Moving to S3 + CloudFront dropped their origin bandwidth by 94%. Genuinely transformative for their hosting bill.

---

Incremental Builds and the Rebuild Problem

Here's a real issue with static generation at scale: your editor publishes a post correction at 3pm and has to wait 5 minutes for a full rebuild. That's not acceptable in a newsroom.

A few approaches I've used:

Option 1: Netlify or Vercel with on-demand ISR. Astro supports server-side rendering with adapters — you can run Astro in hybrid mode where most pages are static but specific routes are rendered on-demand. For a news site, I'll often statically pre-render the last 30 days of posts (high traffic, needs speed) and set older archive pages to server-render on demand. Best of both worlds.

Option 2: Webhook-triggered partial builds. WordPress fires a webhook on post save (easy with the WP Webhooks plugin). That webhook hits a Netlify or Vercel deploy hook. The build runs, it fetches only what's changed. It's not truly partial — Astro still rebuilds everything — but if you keep your build fast, 4 minutes is workable.

Option 3: Just use SSR for the whole thing. Deploy Astro with the Node adapter to a VPS (I use Hetzner for this — cheap, fast, reliable). Every page renders on request, you cache aggressively at the Nginx or Cloudflare level, and you have instant post updates. This is what I'd do for a proper publishing operation over 50,000 posts.

Honest opinion? Most sites don't need the complexity of Option 1 or 3. A 4-minute rebuild triggered by a webhook is fine for 90% of content sites.

---

Performance: What You Actually Get

On the publisher project from the opening — here's what happened after the Astro migration:

  • LCP dropped from 7.2s to 1.1s on mobile (tested with WebPageTest from a London node)
  • Total Blocking Time went from ~800ms to 0ms (zero JS by default, remember)
  • Their Google Search Console Core Web Vitals report went from 3% "Good" URLs to 91% "Good" within six weeks of deployment
  • Hosting costs dropped because their WordPress server was no longer serving pages, only API responses

None of that is magic. It's just what happens when you remove PHP rendering from the critical path and stop shipping a 400kb theme JavaScript bundle to every reader.

---

The Bits That Will Trip You Up

Real talk — things I've had to debug on actual projects:

  • Draft post previews. This is genuinely annoying in a headless setup. WordPress's native preview relies on front-end rendering. You need to build a custom preview endpoint in Astro that accepts a WordPress preview nonce and fetches the draft via WPGraphQL. Not hard, but it takes a day to do properly.
  • Redirects. If the old site had hundreds of redirects in .htaccess, those live on the WordPress server now. You need to either replicate them in Astro's config, or keep WordPress accessible and proxy specific paths. I've done both. Replicating in Astro is cleaner long-term.
  • Search. WordPress's built-in search is useless in a headless setup. I use Algolia with the WP Search with Algolia plugin. Index your posts in WP, query Algolia from an Astro Island component. Works well.
  • Menus and navigation. WordPress menus are weirdly fiddly to expose through WPGraphQL. The wpgraphql-acf route often ends up being cleaner — just model your nav as an ACF repeater and call it done.

---

FAQ

Do I need WPGraphQL or can I just use the REST API?

You can absolutely use the REST API — it's built into WordPress core and requires no extra plugins. For simple sites with standard post types and minimal custom fields, it's fine. Where GraphQL earns its place is when you've got complex content models with many custom fields per type. Being able to fetch exactly the fields you need in a single request, without wrestling with _embed parameters and nested REST calls, saves time on every query you write. Up to you. I just find GraphQL cleaner past a certain complexity threshold.

How do I handle WordPress authentication for members-only content?

JWT authentication is the standard approach. Install the JWT Authentication for WP REST API plugin, issue tokens on login, pass them in your GraphQL request headers. On the Astro side, you'd handle this with an SSR route (not static) so the user-specific content is fetched server-side per request. Don't try to do this statically — that way lies madness.

Is this overkill for a small blog?

Yes, probably. If you've got under 500 posts and one editor, the overhead of maintaining a headless setup isn't worth it. Just use a good WordPress theme, optimise your images, and get on with your life. This architecture pays off when you've got volume, editorial complexity, or traffic levels where performance genuinely moves the needle on revenue.

What's the hosting setup look like in production?

WordPress (CMS only) on a small VPS or managed WordPress host — I use Kinsta or Cloudways. Astro frontend on Vercel, Netlify, or a Hetzner VPS with Nginx depending on the project. Cloudflare in front of everything. Total monthly cost for a mid-sized content site is usually £60–£120, which is often less than what clients were paying for an all-in-one WordPress host that was struggling under the load.

---

The honest summary is this: headless WordPress with Astro is one of the better things to happen to content sites in a while. Not because it's new, but because the tooling has finally caught up to the idea. WPGraphQL is stable, Astro's build system is fast, and the performance gains are real and measurable.

Get the architecture right early — especially your image strategy and your rebuild approach — and you'll spend far less time firefighting later. That's really it.

< BACK TO BLOG