Front-end education for the real world. Since 2018.





Completing the WordPress headless CMS integration

Andy Bell

Project: Personal website redesign

Sprint: Skeletal build


Let’s work out what I need to account for here by referring back to our existing page audit.

A Penpot file that has every page type from my existing site mapped out next to each other in labelled sections

I need to account for the following:

  1. Post listings, limited by a defined items per page
  2. Pagination to render each page of post listings
  3. The actual blog items themselves

With those, I’d consider this base implementation done, which is where I want to be in this iteration.

A pagination shell component

Code language
astro

---
const { next, previous } = Astro.props;
---

{
  (previous || next) && (
    <div class="wrapper">
      <nav class="repel" aria-label="Pagination links">
        {previous ? <a href={previous}>Newer posts</a> : <>&nbsp;</>}
        {next && <a href={next}>Older posts</a>}
      </nav>
    </div>
  )
}

I’ve used a <nav> here, so it needs an appropriate label: “Pagination links”. The logic is quite straightforward here:

  1. Attempt to grab next and previous props, which are URLs
  2. Test if either next or previous have anything for us
  3. If so, we’re safe to render any wrapping markup because there will be at least one link
  4. If previous is good to go, render the link, if not render a non-breaking space (I’ll explain in a moment)
  5. If next is good to go, render the link

The repel CSS class is a composable layout, which is part of our CSS system, that pushes items away from each other on the inline axis where there’s available space. If there’s not enough space available, the elements stack vertically, with configurable space.

The reason there’s a non-breaking space is if there’s no previous link, we still want to push the “Older posts” link out to the right. You might see a hack here but I see it as using the tools the browser gives us to keep everything simple.

Rigging up the paginated blog index

All the parts are ready now, so I can wire it up and move on to the next thing.

Astro comes with handy pre-baked pagination functionality so all I need to do here is let the system know what pages I need building. This is done with a [...page].astro file, which tells Astro to use rest parameters for this dynamic route.

Code language
astro

---
import { fetchAllWordPressPosts } from '@repo/data/wordPressData';
import PageLayout from 'src/layouts/PageLayout.astro';
import PostListRegion from '@repo/ui/PostListRegion';
import Pagination from '@repo/ui/Pagination';

export async function getStaticPaths({ paginate }) {
  const posts = await fetchAllWordPressPosts();
  return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;
---

<PageLayout title="Blog" summary="" socialImage="" allowRobots={true}>
  <PostListRegion posts={page.data} />
  <Pagination previous={page.url.prev} next={page.url.next} />
</PageLayout>


This is it. It’s all we need to have a paginated blog index. The perks of building in small parts, eh?

Let’s just zoom in on this part:

Code language
js

export async function getStaticPaths({ paginate }) {
  const posts = await fetchAllWordPressPosts();
  return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;

My website is going to be 99% statically generated, which means the output is HTML pages in directories. In order for Astro to render these pages, it needs to know about them. To do that, we need to return paths (AKA relative URLs) to the system, using the getStaticPaths function.

Using a destructuring assignment we can pull out the paginate function from the available arguments. We then use that to paginate posts — which is our familiar fetchAllWordPressPosts() data — limiting each page to 10 items. Paginate then supplies each path, which results in pages, like this one on the live website.

Blog post items

We’ve got the means to navigate around the blog now, but not the means to actually read the posts. Let’s fix that with another simple astro file.

Code language
astro

---
import { fetchAllWordPressPosts } from '@repo/data/wordPressData';
import { stripHTML, decodeHTMLEntities } from '../utils/helpers';
import PageLayout from 'src/layouts/PageLayout.astro';
import PostRegion from '@repo/ui/PostRegion';

export async function getStaticPaths() {
  const posts = await fetchAllWordPressPosts();
  return posts.map((entry) => ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}

const { entry } = Astro.props;
---

<PageLayout
  title={decodeHTMLEntities(entry.title.rendered)}
  summary={decodeHTMLEntities(stripHTML(entry.excerpt.rendered))}
  socialImage={entry?.jetpack_featured_media_url}
  allowRobots={true}
>
  <PostRegion
    title={decodeHTMLEntities(entry.title.rendered)}
    date={entry.date}
    content={entry.content.rendered}
  />
</PageLayout>

This is all very familiar, right? Fundamentally, there’s not much difference going on here than in the paginated blog index file. Instead of paginating the WordPress data, we’re rendering a page for each blog post and passing the data into other components to do the rendering for us.

Cleaning up content

Let’s just take a look at a couple of helper functions which might have caught your attention decodeHTMLEntities() and stripHTML(). Unfortunately, there’s quite a lot of crap when it comes to WordPress content, especially over the API.

Code language
js

export function decodeHTMLEntities(input) {
  return input.replace(/&#(\d+);/g, (match, dec) => {
    return String.fromCharCode(dec);
  });
}

This function grabs the unicode number from each encoded match then replaces the encoded part with a proper, readable character. It’s mainly titles and summaries where this applies because WordPress does some weird stuff with those.

Code language
js

export function stripHTML(input) {
  return input.replace(/<[^>]*>?/gm, '');
}

This is the Ronseal of JavaScript functions: it does exactly what it says on the tin. It takes a string of HTML and returns back plain text.

I used this function in conjunction with decodeHTMLEntities() because entry.excerpt.rendered is both HTML and riddled with HTML entities — both of which I don’t want.

There’s no need to clean up entry.content.rendered because we do want the HTML for that. I’m using Astro’s set:html in that context again, in the <PostRegion />, which accounts for any nasties for me.

Here we have it, the paginated index: A very rough looking list of blog post links, with newer and older pagination links

And here’s an item: A very rough looking blog post, rendered as simple HTML

Looks pretty bland! But that’s fine, we’ll come to that later.

It’s all about AT protocol/Bluesky integration now, while I’m already in the mood to integrate 3rd party stuff.

See you in the next one!

Enjoyed this article? You can support us by leaving a tip via Open Collective


About