Integrating WordPress Posts with 11ty

If you’ve followed me around the internetz it comes as no surprise that I’m a huge fan of 11ty – a JavaScript based static-site generator that’s super flexible, can be extended, and is really fun to work with.

When I switched my 11ty based website to WordPress the main reason was that I’ve grown tired of writing Markdown files and I wanted something with a backend again, something where I could draft an article on-the-go and finish it later on my computer and have proper Media management as well; Media management especially was (and is) annoying without a proper CMS in my opinion. While I’m happy with WordPress as a CMS, I’m not so happy with theming and PHP. I really don’t want to write a theme in PHP (like, really really don’t want to!) so I’ve rolled with the default theme (Twenty Twenty-Three) which is fine and does it’s job, but I miss the fun in creating and building websites.

Besides the WordPress based kevingimbel.de I’ve also had a 11ty site published at kevin.gimbel.dev/ops – a little site showcasing some of the technologies I’ve worked with, and mainly created as a showcase (and for fun!) when I was looking for a new job in 2022.

Well, longest story short: I just finished integrating this blog (even THIS ARTICLE) automatically with my 11ty site! So all articles are now published on kevin.gimbel.dev/blog as well.

Screenshot showing the blog page on kevin.gimbel.dev
It's white boxes with text on a blue background.

Visually it isn’t anything to brag to your Front-End Friends about, but it does the job – it shows content.

So without further ado, let’s get to the technical side of it all!

The Goal

… is to have 11ty render pages from posts fetched from the WordPress API. The solution described here doesn’t render WordPress pages yet (tho maybe I’ll implement this in the future).

Creating pages from data

11ty can create pages from data – which means we can use plain old JavaScript to parse a RSS feed (or JSON feed), then turn it into a bunch of pages.

The magic happens in the data file at src/_data/blog.js. The _data directory is a special directory for all … data. Whatever the JavaScript file returns is available inside of all 11ty pages and templates, usable as {{ blog }} (same name as the file).

The script below is all it takes. I use the node-fetch module to load the last 100 posts from kevingimbel.de, then I extract the fields I want to use (title, link, content, excerpt, and slug), and finally at the end of the file the whole Promise is exported.

const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const response = fetch('https://kevingimbel.de/wp-json/wp/v2/posts?orderby=date&order=desc&per_page=100');

let data = response.then((response) => {
    return response.json();
}).then(data => {
    let _posts = [];
    data.forEach(item => {
        var post = {
            title: item.title.rendered,
            link: item.link,
            content: item.content.rendered.replace("comment below 👇", `<a href="${item.link}#respond">comment on the original article on kevingimbel.de</a>`),
            excerpt: item.excerpt.rendered,
            slug: item.link.replace('https://kevingimbel.de/', '')
        }
        _posts.push(post);

    });

    return _posts;
}).catch(err => {
    console.log("Error retrieving posts: ", err);
});

module.exports = data;

Some interesting parts in the script:

content

In the content field I replace the string “comment below 👇” with a link to the original article’s comment box (on kevingimbel.de, the WordPress site). This is because the 11ty site has no comments and I don’t want to implement comments there (maybe I can integrate the WordPress comments somehow 🤔).

item.content.rendered.replace("comment below 👇", `<a href="${item.link}#respond">comment on the original article on kevingimbel.de</a>`),

slug

I wanted to have the same URLs as on kevingimbel.de so for the slug I just removed the https://kevingimbel.de/ prefix from the links – the slug is later used in the template files.

slug: item.link.replace('https://kevingimbel.de/', '')

11ty templates

Data alone isn’t enough, we also need to write some templates. There are two templates (I use Nunjucks, but it’s basically the same for every supported template language).

  • blog-page.njk which is the single page of a blog entry
  • blog.njk which is the blog list (feed)

blog-page.njk (Github Link)

The important part is the frontmatter – the header of the template.

---
pagination:
    data: blog
    size: 1
    alias: article
permalink: "{{ article.slug }}"
---

Here we reference the data (data: blog) we defined in src/_data/blog.js, use it as pagination and define a new variable named article (alias: article) which is a reference to the current article being rendered.

The permalink is where the slug from above comes into play: For each article we need to define a permalink because 11ty cannot automatically determine a path, and here we use the value of article.slug which is the URL path from WordPress.

The single page blog template itself is pretty much “standard” 11ty code:

{% extends "base.njk" %}

{% block head %}
<link rel="canonical" href="{{ article.link }}" />
{% endblock %}

{% block content %}

<section class="[ f-ultra bg-accent-2 stack ] [ gr-auto gc-full ] [ c-full-width ] single-page article-page">
    {% include "nav.njk" %}

    <div class="article__og-box">
        <p><i>"{{ article.title}}"</i> was originally published on <a href="{{ article.link }}" target="_blank" rel="nofollow" title="Read '{{ article.title}}' on kevingimbel.de">kevingimbel.de</a>.</p>
    </div>
    <hr />
    <div class="content">
        <h1>{{ article.title }}</h1>
        {{ article.content | safe }}
    </div>
</section>

{% endblock %}

I’m using {{ article.content | safe }} here to render the HTML retrieved from the WordPress API.

One not-so-standard thing is the canonical link. This link is added to the head of the HTML page and references the original article published on kevingimbel.de.

{% block head %}
<link rel="canonical" href="{{ article.link }}" />
{% endblock %}

In my base template I have a block named head defined in the HTML head to dynamically add new attributes to it. (Code on GitHub).

blog.njk

And lastly, the blog.njk page is where the blog list is rendered.

{% extends "base.njk" %}

{% block content %}

<section class="[ f-ultra bg-accent-2 stack ] [ gr-auto gc-full ] [ c-full-width ]  single-page">
    <div class="content">
        {% include 'nav.njk' %}
        <hr />

        <ul class="article-list">
        {% for article in blog %}
        <li class="article-list__item">
            <h2><a href="/{{ article.slug }}">{{ article.title }}</a></h2>
            <p>{{ article.excerpt | safe }}</p>
            <a href="/{{ article.slug }}" class="article-list__more-link">Continue reading &gt;&gt;</a>
        </li>
        {%- endfor -%}
        </ul>
    </div>
</section>

{% endblock %}

Conclusion

Integrating WordPress blog posts with 11ty is surprisingly straight forward, thanks to the WordPress JSON API and 11ty data pages there was little custom code needed.

There are still some TODOs:

  • Only 100 posts are loaded at the moment, I’d like to load more and possibly cache the results somehow – content rarely changes so it would be fine to only load the last posts published
  • I’d like to trigger a GitHub Action every time I publish a post
  • The blog list looks boring, I want to make this more fun!

Links