Astro and WordPress are a natural pair: WordPress gives your editors the admin they already know, and Astro turns that content into fast, mostly static pages with almost no client JavaScript. This is the setup we use at Seahawk for content-heavy enterprise sites, written as the steps you would actually follow, with Spacefast as the deploy target at the end.
Before you start
You need a WordPress install you can add plugins to, a Node environment on a version current Astro supports (check the Astro docs), and a Git host. Keep WordPress on its own domain or subdomain such as cms.example.com; the Astro site will own the public domain.
Step 1: Expose content with WPGraphQL
- Install and activate the WPGraphQL plugin. Your endpoint is now
https://cms.example.com/graphql. - If you use Advanced Custom Fields, install WPGraphQL for ACF and enable “Show in GraphQL” on each field group.
- Open the GraphiQL IDE in the WordPress admin and run a test query:
query Posts {
posts(first: 10) {
nodes {
id
slug
title
date
excerpt
content
featuredImage { node { sourceUrl altText } }
}
}
}
- Harden the endpoint: in WPGraphQL settings, disable public introspection in production, enable query depth limits, and restrict the endpoint to GET and POST from your build and preview origins at the CDN or WAF.
Step 2: Create the Astro project
npm create astro@latest my-site
cd my-site
npm install
Pick the minimal template. Add an .env file with the endpoint and a token for authenticated preview requests:
WP_GRAPHQL_URL=https://cms.example.com/graphql
WP_PREVIEW_TOKEN=replace-me
Never commit the token. Set it in your host’s environment variables.
Step 3: Write a content loader
Astro’s Content Layer lets you define a collection backed by any source. Create src/content.config.ts:
import { defineCollection, z } from "astro:content";
const posts = defineCollection({
loader: async () => {
const res = await fetch(import.meta.env.WP_GRAPHQL_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{ posts(first: 1000) { nodes {
id slug title date excerpt content
featuredImage { node { sourceUrl altText } }
} } }`,
}),
});
const { data } = await res.json();
return data.posts.nodes.map((p) => ({ ...p, id: p.slug }));
},
schema: z.object({
slug: z.string(),
title: z.string(),
date: z.string(),
excerpt: z.string().nullable(),
content: z.string().nullable(),
featuredImage: z
.object({ node: z.object({ sourceUrl: z.string(), altText: z.string().nullable() }) })
.nullable(),
}),
});
export const collections = { posts };
For sites over a few thousand posts, paginate with after and pageInfo.endCursor instead of one large query, and consider persisting the loader result so unchanged entries are skipped on rebuild.
Then render the collection in src/pages/[slug].astro:
---
import { getCollection, getEntry } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("posts");
return posts.map((p) => ({ params: { slug: p.data.slug } }));
}
const post = await getEntry("posts", Astro.params.slug);
---
<article>
<h1>{post.data.title}</h1>
<Fragment set:html={post.data.content} />
</article>
WordPress block HTML renders cleanly with set:html. Sanitize it at build time if contributors can embed arbitrary markup, and rewrite internal cms.example.com links to the public domain.
Step 4: Rebuild on publish
Editors expect publish to mean live. Wire WordPress to your host’s build hook:
- Create a deploy hook URL in your hosting dashboard.
- In WordPress, add a small plugin or
mu-pluginthat calls the hook ontransition_post_statuswhen a post becomes published, is updated while published, or is unpublished:
add_action('transition_post_status', function ($new, $old, $post) {
if ($new === 'publish' || $old === 'publish') {
wp_remote_post(getenv('ASTRO_DEPLOY_HOOK'), ['blocking' => false]);
}
}, 10, 3);
- Debounce on the host side if your team publishes in bursts; most platforms queue and collapse concurrent builds.
Full builds of a few thousand pages typically finish in a minute or two. If your site is much larger, split the long tail into server-rendered routes with edge caching and keep only the top pages static.
Step 5: Editor previews
Point WordPress’s Preview button at Astro:
- Filter
preview_post_linkin WordPress to returnhttps://www.example.com/preview?id={post_id}&token={signed token}. Sign the token with a shared secret and a short expiry. - Add
src/pages/preview.astroas a server-rendered route (setexport const prerender = false). It verifies the token, then queries WPGraphQL withasPreview: trueusing an application password or JWT in theAuthorizationheader to fetch the latest revision. - Render with the same layout and components as the published route.
- Send
Cache-Control: no-storeandX-Robots-Tag: noindexfrom the preview route.
Ship preview in the first sprint. It is the feature that earns editor trust.
Step 6: Images and media
Keep media in WordPress, but serve it well. Either proxy sourceUrl through Astro’s <Image> component with the CMS domain allow-listed in astro.config.mjs, or put the WordPress uploads folder behind your CDN with image resizing on the edge. Always carry altText through; accessibility audits will find the gap otherwise.
Step 7: Deploy
Push the repository and connect it to Spacefast. Spacefast detects Astro, sets the build command, and gives you a deploy hook URL to drop into the WordPress plugin from Step 4. Add your environment variables, map the production domain, and enable the server adapter if you used a preview route or on-demand rendering. The first deploy runs a full build; later publishes hit the hook and the site refreshes within minutes.
Before launch, confirm:
- The CMS domain is not indexed and only serves the API and admin.
- Redirects from legacy WordPress URLs are handled at the edge.
- The build fails loudly if WPGraphQL is unreachable, instead of shipping an empty site.
Next step
Spacefast, a Seahawk partner, is Astro-native hosting, so the deploy hooks, environment handling, and edge caching this guide relies on are first-class rather than bolted on. If your WordPress side needs architecture help, a hardened WPGraphQL setup, or an enterprise host such as WordPress VIP behind the API, Seahawk VIP handles that half of the stack.