DEV Community

Cover image for Building an Astro Website with WordPress as a Headless CMS
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Originally published at blog.openreplay.com

Building an Astro Website with WordPress as a Headless CMS

by author Chris Bongers

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:

mkdir wordpress && cd wordpress
Enter fullscreen mode Exit fullscreen mode

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

version: '3.9'

services:
  db:
    image: mariadb
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped
    ports:
      - 3307:3306
    environment:
      MYSQL_ROOT_PASSWORD: rootpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    volumes:
      - wordpress_data:/var/www/html
    ports:
      - '8000:80'
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
volumes:
  db_data: {}
  wordpress_data: {}
Enter fullscreen mode Exit fullscreen mode

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

docker-compose up
Enter fullscreen mode Exit fullscreen mode

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.

mkdir astro-wordpress && cd astro-wordpress
Enter fullscreen mode Exit fullscreen mode

Then we can install Astro by running the following command:

npm init astro
Enter fullscreen mode Exit fullscreen mode

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:
npm install --D tailwindcss
Enter fullscreen mode Exit fullscreen mode
  • Create a tailwind.config.js file
module.exports = {
  mode: 'jit',
  purge: ['./public/**/*.html', './src/**/*.{astro,js,jsx,svelte,ts,tsx,vue}'],
};
Enter fullscreen mode Exit fullscreen mode
  • Enable tailwind config in your astro.config.mjs file.
export default {
  devOptions: {
    tailwindConfig: './tailwind.config.js',
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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

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

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:

npm install @tailwindcss/typography
Enter fullscreen mode Exit fullscreen mode

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

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

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.

npm install --D dotenv
Enter fullscreen mode Exit fullscreen mode

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

WP_URL=http://localhost:8000/graphql
Enter fullscreen mode Exit fullscreen mode

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.

import dotenv from 'dotenv';
dotenv.config();
const API_URL = process.env.WP_URL;
Enter fullscreen mode Exit fullscreen mode

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.

async function fetchAPI(query, { variables } = {}) {
  const headers = { 'Content-Type': 'application/json' };
  const res = await fetch(API_URL, {
    method: 'POST',
    headers,
    body: JSON.stringify({ query, variables }),
  });

  const json = await res.json();
  if (json.errors) {
    console.log(json.errors);
    throw new Error('Failed to fetch API');
  }

  return json.data;
}
Enter fullscreen mode Exit fullscreen mode

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

export async function getAllPagesWithSlugs() {
  const data = await fetchAPI(`
  {
    pages(first: 10000) {
      edges {
        node {
          slug
        }
      }
    }
  }
  `);
  return data?.pages;
}
Enter fullscreen mode Exit fullscreen mode

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.

export async function getPageBySlug(slug) {
  const data = await fetchAPI(`
  {
    page(id: "${slug}", idType: URI) {
      title
      content
    }
  }
  `);
  return data?.page;
}
Enter fullscreen mode Exit fullscreen mode

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:

---
Code
---
<html>
    <h1>HTML</h1>
</html>
Enter fullscreen mode Exit fullscreen mode

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

---
import { getAllPagesWithSlugs, getPageBySlug } from '../lib/api';
---
Enter fullscreen mode Exit fullscreen mode

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:

export async function getStaticPaths() {
  const pagesWithSlugs = await getAllPagesWithSlugs();
}
Enter fullscreen mode Exit fullscreen mode

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

export async function getStaticPaths() {
  const pagesWithSlugs = await getAllPagesWithSlugs();
  return pagesWithSlugs.edges.map(({ node }) => {
    return {
      params: { slug: node.slug },
    };
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

const { slug } = Astro.request.params;
const page = await getPageBySlug(slug);
Enter fullscreen mode Exit fullscreen mode

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

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{page.title}</title>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" type="text/css" href={Astro.resolve('../styles/global.css')} />
  </head>
  <body>
    <div class="flex flex-col p-10">
      <div class="mb-5 text-4xl font-bold">{page.title}</div>
      <article class="prose lg:prose-xl">
        {page.content}
      </article>
    </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

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.

export async function getPrimaryMenu() {
  const data = await fetchAPI(`
  {
    menus(where: {location: PRIMARY}) {
      nodes {
        menuItems {
          edges {
            node {
              path
              label
              connectedNode {
                node {
                  ... on Page {
                    isPostsPage
                    slug
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  `);
  return data?.menus?.nodes[0];
}
Enter fullscreen mode Exit fullscreen mode

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.

---
import { getPrimaryMenu } from '../lib/api';

const { menuItems } = await getPrimaryMenu();
---
Enter fullscreen mode Exit fullscreen mode

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

Next up the HTML section for this:

<nav class="flex flex-wrap items-center justify-between p-6 bg-blue-500 shadow-lg">
  <a href="/" class="cursor-pointer p-4 ml-2 text-white">AstroPress</a>
  <ul class="flex items-center justify-end flex-grow">
    {menuItems.edges.map((item) => 
        <li key={item.node.path}>
            <a href={item.node.connectedNode.node.slug} class={`cursor-pointer p-4 ml-2 text-white`}>
                {item.node.label}
            </a>
        </li>
    )}
  </ul>
</nav>
Enter fullscreen mode Exit fullscreen mode

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

---
import Header from '../components/Header.astro';
---
Enter fullscreen mode Exit fullscreen mode

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

<body>
    <Header />
    <!-- Other code -->
</body>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)