OpenReplay
Navigate back to the homepage
BLOG
Browse Repo
Back

Building an Astro Website with WordPress as a Headless CMS

Chris Bongers
October 8th, 2021 · 6 min read

Hi everyone!

I’m sure you’ve heard of WordPress, and you may or may not like it. In this article, I’ll show you how you can leverage only the good parts of WordPress by using it as a headless CMS for your next project.

As the front-end, we’ll be using the new popular static site generator Astro.

Let’s dive a bit into the details of what we are working with.

What is Astro?

Astro is a Static Site Generator (SSG), meaning it can output a static HTML website.

You might be wondering, ok, but why do we need that? An SSG is excellent since it outputs static HTML, which in return means your website will be blazing fast. There is nothing faster than a plain HTML website.

We often want dynamic parts and components on our website. That’s where SSG comes in handy.

Astro is quite the new kid on the block, yet very powerful and full of potential. Here are some benefits to using Astro:

  • SEO Focused out of the box
  • BYOF (Bring your own framework) approach, bring which ever framework you like to work in, and Astro makes it work
  • Partial hydration, making components render at the right time
  • Lots of built-in support
  • Routing is very extended
  • Active community

These are just some of the reasons Astro is pretty amazing if you ask me. But if you wonder how Astro compares to other tools, check out this amazing document they set up.

What is a headless CMS?

Now that we have the front-end part explained let’s take a moment to check out what precisely is a Headless CMS.

I’m sure you’ve heard about WordPress, the bloated and well-used CMS system. WordPress is an absolute package monster, allowing people to manage their websites with little developer experience.

The development community often dislikes WordPress because it gets a bit too bloated. Meaning the websites are slow and full of stuff we don’t need.

That’s where WordPress as a headless system comes in. A headless system means you can use the entire backend system of WordPress but you don’t have to use the front-end output.

Instead, we use an API to query our data and use it in other system. In our case that would be an Astro front-end.

For the API system, we’ll use GraphQL as the query language, but more on that in the step below.

Setting up WordPress as a headless CMS

Before we continue, let’s set up WordPress and especially set it up as a Headless CMS.

The easiest way to set up WordPress on your local machine is to use a docker image.

If you don’t have Docker Desktop installed follow this guide on the Docker website.

Next up, create a new folder and navigate to it:

1mkdir wordpress && cd wordpress

Then create a docker-compose.yml file and fill out the following details:

1version: '3.9'
2
3services:
4 db:
5 image: mariadb
6 volumes:
7 - db_data:/var/lib/mysql
8 restart: unless-stopped
9 ports:
10 - 3307:3306
11 environment:
12 MYSQL_ROOT_PASSWORD: rootpress
13 MYSQL_DATABASE: wordpress
14 MYSQL_USER: wordpress
15 MYSQL_PASSWORD: wordpress
16
17 wordpress:
18 depends_on:
19 - db
20 image: wordpress:latest
21 volumes:
22 - wordpress_data:/var/www/html
23 ports:
24 - '8000:80'
25 restart: always
26 environment:
27 WORDPRESS_DB_HOST: db:3306
28 WORDPRESS_DB_USER: wordpress
29 WORDPRESS_DB_PASSWORD: wordpress
30 WORDPRESS_DB_NAME: wordpress
31volumes:
32 db_data: {}
33 wordpress_data: {}

Then we can spool up our docker image by running the following command:

1docker-compose up

Once it’s up, you should see the following in your Docker Desktop client.

Docker running WordPress instance

The next step is to visit our WordPress installation and follow the install steps.

You can find your WordPress installation on ‌http://localhost:8000/ and should be welcomed by the WordPress install guide.

WordPress install guide

To set it up as a Headless CMS, we need to install the WP GraphQL plugin.

WP GraphQL plugin

Follow the install guide of the plugin. Once it’s installed, we even get this fantastic GraphQL editor to test out our queries.

GraphQL Editor in WordPress

And we get a GraphQL endpoint available at the following URL: http://localhost:8000/graphql.

While you are in the WordPress section, create some demo pages. Next up it’s time to set up our Astro project.

Setting up the Astro project

Please create a new folder and navigate to it.

1mkdir astro-wordpress && cd astro-wordpress

Then we can install Astro by running the following command:

1npm init astro

You can choose the start template to get started with. Next up, run npm install to install all dependencies and start up your Astro project by running npm start.

You can now visit your front-end at http://localhost:3000/.

Astro working demo website

Adding Tailwind CSS as our styling framework

Right, before we move on to loading our WordPress data, let’s install TailwindCSS as it will make our lives easier in styling the website.

Installing Tailwind in an Astro project is pretty easy. Let’s see what needs to happen step by step.

  • Install Tailwind:
1npm install --D tailwindcss
  • Create a tailwind.config.js file
1module.exports = {
2 mode: 'jit',
3 purge: ['./public/**/*.html', './src/**/*.{astro,js,jsx,svelte,ts,tsx,vue}'],
4};
  • Enable tailwind config in your astro.config.mjs file.
1export default {
2 devOptions: {
3 tailwindConfig: './tailwind.config.js',
4 }
5};

And lastly, we need to create a styles folder in the src directory. Inside this creates a global.css file and add the following contents:

1@tailwind base;
2@tailwind components;
3@tailwind utilities;

To use this style in our pages, we need to load it like so:

1<link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/global.css')} />

Installing the Tailwind typography plugin

Seeing as our content comes from WordPress, we can leverage the Tailwind Typography plugin to not have to style things manually.

Run the following command to install the plugin:

1npm install @tailwindcss/typography

Then open your tailwind.config.js file and add the plugin:

1module.exports = {
2 mode: 'jit',
3 purge: ['./public/**/*.html', './src/**/*.{astro,js,jsx,svelte,ts,tsx,vue}'],
4 plugins: [require('@tailwindcss/typography')],
5};

And that’s it! We can now use Tailwind and its fantastic typography plugin.

Creating a .env file

Since our endpoint might vary depending on our environment, let’s install the dotenv package.

1npm install --D dotenv

Then we can create a .env file that will contain our WordPress graphQL endpoint.

1WP_URL=http://localhost:8000/graphql

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

Creating the API calls in Astro

Alright, we have our WordPress set up and our basic Astro website up and running. It’s time to bring these two together.

Create a lib folder in the src directory and create a file called api.js.

This file will contain our API calls to the WordPress GraphQL API endpoint.

The first thing we need to do in this file is loading our environment.

1import dotenv from 'dotenv';
2dotenv.config();
3const API_URL = process.env.WP_URL;

Then we need to create a basic fetchAPI call that will execute our GraphQL queries. This generic call will handle the URL and actual posting.

1async function fetchAPI(query, { variables } = {}) {
2 const headers = { 'Content-Type': 'application/json' };
3 const res = await fetch(API_URL, {
4 method: 'POST',
5 headers,
6 body: JSON.stringify({ query, variables }),
7 });
8
9 const json = await res.json();
10 if (json.errors) {
11 console.log(json.errors);
12 throw new Error('Failed to fetch API');
13 }
14
15 return json.data;
16}

Then let’s create a function that can fetch all our WordPress pages that have a slug.

1export async function getAllPagesWithSlugs() {
2 const data = await fetchAPI(`
3 {
4 pages(first: 10000) {
5 edges {
6 node {
7 slug
8 }
9 }
10 }
11 }
12 `);
13 return data?.pages;
14}

As you can see, we pass a GraphQL query to our fetchAPI function and return all the pages we get in return.

Remember you can try out these GraphQL queries in the WordPress plugin GraphQL viewer.

Seeing the above query will only give us the slugs for each page. We can go ahead and create a detailed call that can retrieve a page’s content based on its slug.

1export async function getPageBySlug(slug) {
2 const data = await fetchAPI(`
3 {
4 page(id: "${slug}", idType: URI) {
5 title
6 content
7 }
8 }
9 `);
10 return data?.page;
11}

Rendering WordPress pages in Astro

Now that we have these functions set up, we need to create these pages in our front-end Astro project dynamically.

Remember how Astro outputs static HTML? That means we need a way to retrieve these and dynamically build these pages.

Luckily Astro can do just that for us!

To create a dynamic page, we must create a file called [slug].astro in our pages directory.

As this is an Astro file, it comes in two sections, the code, and the HTML. The code is wrapped in frontmatter (three lines), and it looks like this:

1---
2Code
3---
4<html>
5 <h1>HTML</h1>
6</html>

Let’s first import the two functions we need from our API file.

1---
2import { getAllPagesWithSlugs, getPageBySlug } from '../lib/api';
3---

Then Astro comes with a getStaticPaths function that enables us to create dynamic pages.

Inside this function we can wrap all our pages like so:

1export async function getStaticPaths() {
2 const pagesWithSlugs = await getAllPagesWithSlugs();
3}

And then, we can map those to return a slugged page for each of our WordPress pages.

1export async function getStaticPaths() {
2 const pagesWithSlugs = await getAllPagesWithSlugs();
3 return pagesWithSlugs.edges.map(({ node }) => {
4 return {
5 params: { slug: node.slug },
6 };
7 });
8}

You can see the file name must match with the params there, as we have [slug] as the filename. The params must also be slug.

Then the last thing we need is to fetch the current page based on the slug.

1const { slug } = Astro.request.params;
2const page = await getPageBySlug(slug);

Then we can move to the HTML part to render the page!

1<html lang="en">
2 <head>
3 <meta charset="UTF-8" />
4 <title>{page.title}</title>
5 <meta name="viewport" content="width=device-width" />
6 <link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/global.css')} />
7 </head>
8 <body>
9 <div class="flex flex-col p-10">
10 <div class="mb-5 text-4xl font-bold">{page.title}</div>
11 <article class="prose lg:prose-xl">
12 {page.content}
13 </article>
14 </div>
15 </body>
16</html>

You should now be able to visit any of your slugs. Let’s see my privacy policy page, for instance.

Astro rendered page

Loading the primary WordPress menu in Astro

It’s pretty cool that we have these pages at our disposal, but we can’t tell the user to type in the URLs they want to visit.

So let’s create a primary menu in WordPress and use that instead!

First, head over to your WordPress admin panel and find the Appearance > Menu section.

Add a new menu. You can give this any name you want. However, for the display location, choose Primary menu.

WordPress primary menu

You can then go ahead and add some pages to this menu.

The next thing we need to do is query this menu in our lib/api.js file in our front-end project.

1export async function getPrimaryMenu() {
2 const data = await fetchAPI(`
3 {
4 menus(where: {location: PRIMARY}) {
5 nodes {
6 menuItems {
7 edges {
8 node {
9 path
10 label
11 connectedNode {
12 node {
13 ... on Page {
14 isPostsPage
15 slug
16 }
17 }
18 }
19 }
20 }
21 }
22 }
23 }
24 }
25 `);
26 return data?.menus?.nodes[0];
27}

To use this, let’s create a new component that we can re-use. Remember that’s one of the powers Astro brings us.

Create a Header.astro file in your components directory. In there, let’s first go to the code section.

1---
2import { getPrimaryMenu } from '../lib/api';
3
4const { menuItems } = await getPrimaryMenu();
5---

This will retrieve all the menu items in the primary menu we just defined.

Next up the HTML section for this:

1<nav class="flex flex-wrap items-center justify-between p-6 bg-blue-500 shadow-lg">
2 <a href="/" class="cursor-pointer p-4 ml-2 text-white">AstroPress</a>
3 <ul class="flex items-center justify-end flex-grow">
4 {menuItems.edges.map((item) =>
5 <li key={item.node.path}>
6 <a href={item.node.connectedNode.node.slug} class={`cursor-pointer p-4 ml-2 text-white`}>
7 {item.node.label}
8 </a>
9 </li>
10 )}
11 </ul>
12</nav>

To use this component and see it in action, let’s open up the [slug].astro file and import it in our code section.

1---
2import Header from '../components/Header.astro';
3---

Then we can use it in our HTML section by adding the following code in our body tag.

1<body>
2 <Header />
3 <!-- Other code -->
4</body>

And if we refresh our project, we have a super cool menu!

Header menu from WordPress to Astro

Conclusion

Today, we learned how to set up WordPress as a headless CMS and how to load this through a GraphQL endpoint in an Astro website.

For me, this brings the best of two worlds.

WordPress as an established CMS system, something we don’t want to be rebuilding from scratch. And Astro as the SSG that outputs the fastest possible website for us!

From here, the options are endless as you can retrieve posts, custom elements, and more from WordPress.

If you are interested, you can find the complete code on GitHub. Or check out the sample website here.

More articles from OpenReplay Blog

Understanding React Router with a Simple Blog Application

Learn how React Router works by building a very simple blog

October 8th, 2021 · 6 min read

How Relevant is Still TypeScript in 2021?

Is TypeScript still the best type-safe language for the web in 2021?

October 7th, 2021 · 5 min read
© 2021 OpenReplay Blog
Link to $https://twitter.com/OpenReplayHQLink to $https://github.com/openreplay/openreplayLink to $https://www.linkedin.com/company/18257552