This tutorial walks you through building a fast, modern Astro frontend backed by WordPress as a content source. By the end you’ll have:
- An Astro project pulling posts + pages from any WP REST API.
- Per-route caching via Cloudflare KV.
- A clean deploy to Cloudflare Pages.
1. Create the Astro project
Spin up a new Astro project with the minimal starter:
npm create astro@latest mw-headless -- --template minimal --typescript strict
cd mw-headless
npm install
Add the integrations we’ll need:
npm install @astrojs/cloudflare @astrojs/sitemap
Wire them into astro.config.mjs:
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
integrations: [sitemap()],
site: 'https://your-domain.com',
});
2. Fetch posts from WordPress
WP’s REST API ships out of the box — your posts live at
https://your-wp-site.com/wp-json/wp/v2/posts. Wrap it in a tiny client:
// src/lib/wp.ts
const WP = import.meta.env.WP_REST_URL;
export async function getPosts(perPage = 20) {
const res = await fetch(`${WP}/posts?per_page=${perPage}&_embed=1`);
if (!res.ok) throw new Error(`WP returned ${res.status}`);
return res.json();
}
export async function getPostBySlug(slug: string) {
const res = await fetch(`${WP}/posts?slug=${slug}&_embed=1`);
const arr = await res.json();
return arr[0] ?? null;
}
The _embed=1 flag tells WP to inline the featured image + author so
you don’t make a second round-trip per post.
3. Build the post list page
---
// src/pages/blog/index.astro
import { getPosts } from '../../lib/wp';
const posts = await getPosts();
---
<ul>
{posts.map((p) => (
<li>
<a href={`/blog/${p.slug}`} set:html={p.title.rendered} />
</li>
))}
</ul>
WP returns titles as HTML-encoded strings (“Don’t…”), so use
set:html rather than {p.title.rendered} directly — it’ll decode the
entities. Same goes for content.rendered.
4. Add KV caching
REST calls are 100-300ms from most regions. Wrap the fetch in a KV read-through cache and that drops to under 10ms after the first request:
// src/lib/wp.ts (extended)
export async function getPostsCached(env: any) {
const cached = await env.CACHE.get('posts', 'json');
if (cached) return cached;
const fresh = await getPosts();
await env.CACHE.put('posts', JSON.stringify(fresh), { expirationTtl: 300 });
return fresh;
}
Five-minute TTL is a safe default. If you publish frequently, lower it
or fire a cache-purge webhook from WP on transition_post_status.
5. Deploy
npx wrangler pages deploy dist
Add WP_REST_URL as a Cloudflare Pages environment variable in the
dashboard before your first deploy. CI re-runs every time you push.
Verification
Hit /blog in production. The first request may take 200-400ms (cold
cache + first KV write); subsequent requests should land in ~30ms.
Compare TTFB before/after in Chrome DevTools’ Network panel — that’s
your KV hit/miss bar.
Common errors
| Error | Cause | Fix |
|---|---|---|
WP returned 401 | REST API requires auth (some hosts disable anon access) | Add an Application Password header to the fetch |
Cannot read 'rendered' | Post structure changed (custom REST) | Check _embed=1 is set + handler returns the standard shape |
KV put slow | Worker waiting on KV write | Wrap the put in ctx.waitUntil(...) for fire-and-forget |
What’s next
- Add per-route ISR with Cloudflare’s Cache Rules.
- Wire the
transition_post_statusWP hook to purge KV on publish. - See the Cloudflare KV cache snippet for the generic pattern this builds on.
Get the next one in your inbox. Practical tips, no fluff.
More tutorials
View all-
Block Patterns
Register reusable block compositions — landing-page hero, feature grid, FAQ section — that users insert as a complete unit.
25 min
-
Block Attributes and Serialization
Master the attribute system — types, sources, defaults, and how block data round-trips between the editor, the database, and the front-end.
30 min
-
Building a Block Plugin
Package your block as a proper WordPress plugin — file structure, asset enqueueing, server-side render callbacks, plugin metadata.
35 min