Like many headless WordPress developers, in the last year I have made the switch to building front ends with Astro. Marketing itself as “The web framework for content-driven websites,” Astro has emerged as – in my opinion – the best way to build websites today.
Why Use Astro for Headless WordPress?
What makes Astro so great? Compared with previous tools such as Gatsby or Next.js, Astro does not use React to achieve its high performance (though developers can still implement React components if they so choose). This drastically reduces the amount of JavaScript that is sent to the browser by default.
There are a number of other features that make Astro a pleasure to work with:
- It is built on top of Vite, leveraging exceptionally fast hot-module replacement
- It includes optional features such as prefetching
- It can dynamically generate XML sitemaps for all static pages
- There is a built in Image service to optimize and transform images
- Server integrations with Vercel, Netlify, and others make implementing SSR a breeze
- Supports TypeScript out of the box, and NPM packages can be imported into client-side scripts that are bundled at built time
- Integration for Tailwind CSS
I could go on about the benefits of using Astro, but at the end of the day it gives developers a platform to choose whatever tooling they want to use, with the end result being a highly-optimized and performant website.
In version 4.14.0, Astro introduced a new feature called the Content Layer API, which is the focus of this article.
What is the Content Layer API?
The Content Layer API is the latest development in Astro’s efforts to drastically improve build times for static websites. In previous iterations, Astro introduced Content Collections, which added type-safety and build optimizations to content hosted locally (such as in Markdown files), not only making builds faster but also giving developers autocompletion when building out templates for specific content structures.
The Content Layer API expands these enhancements to content from any source – meaning we can now implement type-safe content collections at scale, pulling data from any external source through an API, including our headless WordPress websites.
Using Astro’s Content Layer API
If you are using Astro version 5.0.0
or greater, the Content Layer is included in Astro.
If you are using version 4.14.0
or a later release of Astro version 4, in order to activate the content layer in your Astro project, update your config to turn it on:
{
experimental: {
contentLayer: true,
}
}
Implementing a Collection and adding it to the Content Layer essentially requires two steps:
- Fetching the data
- Processing the data and adding it to a collection
Fetching Headless WordPress Data
Keeping in mind that Astro allows you to fetch data however you want, for this post I will use a simplified example to fetch a group of pages from WordPress. While WordPress has a built-in REST API, personally I prefer to use WPGraphQL, a free plugin which adds a GraphQL API to your WordPress site, which I will use for this example.
First, I will set up a function to fetch WordPress data from my GraphQL endpoint:
const fetchWordPressAPI = async (query, variables = {}) => {
const WP_API_URL = process.env.WPGRAPHQL_ENDPOINT;
if (!WP_API_URL) {
throw new Error("WPGRAPHQL_ENDPOINT is not set in the environment variables");
}
const response = await fetch(WP_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.errors) {
throw new Error("GraphQL errors: " + JSON.stringify(data.errors));
}
return data.data;
};
Then I will add a getAllPages
method, which returns a structured object with all of my WordPress pages and includes the basic page content, as well as the headless WordPress SEO data (in this example, we are using Rank Math – which means you will need to install the wp-graphql-rank-math
plugin from AxeWP if you wish to use this pattern).
const getAllPages = async () => {
const pageQuery = `
query GetAllPages {
pages {
nodes {
id
slug
title
content(format: RENDERED)
seo {
description
title
openGraph {
image {
url
}
}
}
}
}
}
`;
const pageTransformer = (node) => ({
id: node.id || "",
slug: node.slug || "",
title: node.title || "",
content: node.content || "",
seo: {
description: node.seo?.description || "",
title: node.seo?.title || "",
openGraph: {
image: {
url: node.seo?.openGraph?.image?.url || "",
},
},
},
});
try {
const response = await fetchWordPressAPI(pageQuery);
const pages = response.pages.nodes.map(pageTransformer);
console.log(`Successfully fetched ${pages.length} pages`);
return pages;
} catch (error) {
console.error("Error in getAllPages:", error);
throw new Error(`Failed to fetch pages: ${error.message}`);
}
};
Adding Headless WordPress Data to Astro’s Content Layer
Now that we have a way to fetch our WordPress page data, we need to define a collection by implementing a loader
. This is done by creating a configuration file at src/content/config.ts
.
Defining the Schema
We need to define a Zod schema that matches the content we are fetching from WordPress:
import { z } from "astro:content";
const pageSchema = z.object({
id: z.string(),
slug: z.string(),
title: z.string(),
content: z.string(),
seo: z.object({
description: z.string(),
title: z.string(),
openGraph: z.object({
image: z.object({
url: z.string(),
}).optional(),
}),
}),
});
The schema is part of the magic of making the content layer so fast: it introduces type-safety to our data collections, and gives us autocompletion in our TypeScript files where we use the data.
Creating the Loader
Using Astro’s defineCollection
method, we can create a data object that utilizes our pageSchema
. The load function utilizes our getAllPages
function to query for the page data from WordPress, and uses it to implement a content collection.
import { defineCollection } from "astro:content";
export const pagesCollection = defineCollection({
type: 'data',
schema: pageSchema,
load: async () => {
const pages = await getAllPages();
return pages;
},
});
Adding Our Collection to the Content Layer
Finally, our config.ts
file must export a collections object, which can contain one or many different content collections. For example, we may have a Custom Post Type using Advanced Custom Fields – we can easily separate them in our Content Layer by using a separate collection and loader and exporting them in the collections
object.
export const collections = {
pages: pagesCollection,
};
Using the Content Layer in Our Template Components
Now that we have fetched the data from WordPress, created a Content Collection, and added the Collection to our Content Layer, the last step is to create pages with it and make it beautiful for users.
Since we have an array of page objects available to us in the Collection, we can easily leverage Astro’s getStaticPaths
function to generate a page for each of them. Suppose we have a component at src/pages/[slug].astro
. We can import our collection using the getCollection
method, and once we have generated the page, by passing the individual page as props we can now use our data with type-safety and autocompletion.
---
import { getCollection } from 'astro:content';
import Layout from '../components/Layout.astro';
export async function getStaticPaths() {
const pages = await getCollection('pages');
return pages.map(page => ({
params: { slug: page.slug },
props: { page },
}));
}
const { page } = Astro.props;
---
<Layout title={page.seo.title} description={page.seo.description}>
<article>
<h1>{page.title}</h1>
<div set:html={page.content}></div>
</article>
</Layout>
<style>
article {
max-width: 65ch;
margin: 0 auto;
padding: 1rem;
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
</style>
The Content Layer in the Real World
Now that you have tried out the Content Layer API, you may wonder: this seems great for a practice exercise, but will it stand up in the real world?
I recently implemented the Content Layer on another website with 162 pages, which were a mix of Pages, Posts, and Custom Post Types built with ACF. I found that the build time was 6.63s
– in other words, blazingly fast!
Are you interested in leveraging the power of Astro with headless WordPress for your next website? Feel free to contact me anytime to get started.