Building a Blog With 11ty and WordPress

When I decided to move to the Jamstack, I was sure that I wanted to build something with an API and since I'm quite familiar with WordPress and its API that was an easy choice. But what seemed to be an easy task has had its obstacles.

The initial idea was that working with different APIs would force me to get more comfortable again with fetch and the work with promises. But relying on an API was also a good idea since my choice of technology only lasted some weeks. The switch from Sapper to Eleventy would have been much more work without the blogposts stored in my good old WordPress.

Getting and Processing the Article Data

The articles on this site are stored as a Custom Post Type in the same WordPress that also runs my german blog der tag und ich. WordPress has an integrated REST API that can be accessed from anywhere by a POST request. I'm calling the API from within a JavaScript file in Eleventy's _data folder. After fetching the articles, they are added to a data object that is available in every template. This is a very powerful feature of Eleventy and you should really read the linked docs and see more examples of it's use cases.

But before the articles are available for generating the actual markup of the pages I'm doing some more things. First, I'm picking the parts of the API response that are really necessary for my templates. WordPress delivers lots of information that I don't need. The picked parts are then partially modified for my needs, which includes the formatting of the dates for example.

The other big step is the highlighting of the code snippets in my articles. Sadly, I wasn't able to use the official eleventy-plugin-syntaxhighlight because I'm getting the content of my articles as a complete chunk of HTML. There are ways to modify the output of the API but I'm more or less fine with it at the moment. So I'm using Prism.js after loading the content of an article into an instance of jsdom. You'll see that in a minute.

To speed things a bit up: Here's the code of _data/articles.js that I'm using right now to get the articles from WordPress and preprocess them for Eleventy. I hope my comments are good enough so you understand what I'm doing there in detail. If not — or if you have a suggestion to make something better — please contact me!

const AssetCache = require("@11ty/eleventy-cache-assets");
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const Prism = require("prismjs");

/**
 * Get the articles from WordPress
 * Uses eleventy-cache-assets to speed up build time
 */
async function fetchArticles() {
    try {
        return AssetCache(
            // http://host.docker.internal/wp-json/wp/v2/msme_posts?per_page=100
            "https://www.dertagundich.de/wp-json/wp/v2/msme_posts?per_page=100",
            {
                duration: "1d",
                type: "json"
            }
        );
    } catch (error) {
        console.error(`Error: ${error}`);
        return [];
    }
}

/**
 * Clean up and convert the API response for our needs
 */
async function processPosts(blogposts) {
    return Promise.all(
        blogposts.map(async (post) => {
            // remove HTML-Tags from the excerpt for meta description
            let metaDescription = post.excerpt.rendered.replace(
                /(<([^>]+)>)/gi,
                ""
            );
            metaDescription = metaDescription.replace("\n", "");

            // Code highlighting with Prism
            let content = highlightCode(post.content.rendered);

            // Make relative URLs absolute (would work otherwise on the site, but not in the feed)
            content = content.replace(
                'href="/',
                'href="https://martinschneider.me/'
            );

            // Return only the data that is needed for the actual output
            return await {
                title: post.title.rendered,
                date: post.date,
                formattedDate: new Date(post.date).toLocaleDateString("en-US", {
                    weekday: "long",
                    year: "numeric",
                    month: "long",
                    day: "numeric"
                }),
                rssDate: new Date(post.date).toUTCString(),
                modifiedDate: post.modified,
                slug: post.slug,
                metaDescription: metaDescription,
                excerpt: post.excerpt.rendered,
                content: content,
                categorySlugs: post.msme_categories_slugs
            };
        })
    );
}

/**
 * Use Prism.js to highlight embedded code
 */
function highlightCode(content) {
    // since Prism.js works on the DOM,
    // we need an instance of JSDOM in the build
    const dom = new JSDOM(content);

    let preElements = dom.window.document.querySelectorAll("pre");

    // WordPress delivers a `code`-tag that is wrapped in a `pre`
    // the used language is specified by a CSS class
    if (preElements.length) {
        preElements.forEach((pre) => {
            let code = pre.querySelector("code");

            if (code) {
                // get specified language from css-classname
                let codeLanguage = "html";
                const preClass = pre.className;

                var matches = preClass.match(/language-(.*)/);
                if (matches != null) {
                    codeLanguage = matches[1];
                }

                // save the language for later use in CSS
                pre.dataset.language = codeLanguage;

                // set grammar that prism should use for highlighting
                let prismGrammar = Prism.languages.html;

                if (
                    codeLanguage === "javascript" ||
                    codeLanguage === "js" ||
                    codeLanguage === "json"
                ) {
                    prismGrammar = Prism.languages.javascript;
                }

                if (codeLanguage === "css") {
                    prismGrammar = Prism.languages.css;
                }

                // highlight code
                code.innerHTML = Prism.highlight(
                    code.textContent,
                    prismGrammar,
                    codeLanguage
                );

                code.classList.add(`language-${codeLanguage}`);
            }
        });

        content = dom.window.document.body.innerHTML;
    }

    return content;
}

module.exports = async () => {
    const blogposts = await fetchArticles();
    const processedPosts = await processPosts(blogposts);
    return processedPosts;
};

As you might have noticed, I'm fetching a maximum of one hundred posts. That's the upper limit of the API and that's OK for me now. I have to write ninety more posts until I'm running into a problem and I've dropped the possibility to load more posts in favour of using eleventy-cache-assets. Unfortunately, the plugin does not provide the response headers where WordPress returns the total number of posts and pages which would be needed for a loop. Before using the cache plugin I had an implementation that was pretty close to this solution that Jérôme Coupé build for a GraphQL API. On this page, I'll deal with that problem once I'll be close to one hundred posts. You should definitely subscribe to my RSS feed to stay updated!

Writing the Pages With Eleventy

Building the overview page for the articles was not much more than following the instructions in the 11ty documentation on paginations. You might find the sections "Paging a Collection" and "Remapping with permalinks" useful. Here's the front matter of the overview template, where pagination and permalink are the interesting parts. The actual markup of the list is just an ul filled with article elements.

---
layout: page
title: Articles
pagination:
    data: articles
    size: 10
permalink: articles/index.html
---

The exact same technique is used to display the actual article in its template. See how I've set the size of the pagination to "1" and aliased the pagination for some naming convenience in the markup.

---
layout: page
pagination:
    data: articles
    size: 1
    alias: article
permalink: articles//index.html
---

If you have read other articles on building a blog with 11ty, you might notice that I did not include any front matter for meta descriptions or the title tag in this template. I'm doing that on other pages as well. Unfortunately, front matter cannot be filled from Nunjucks variables. So I had to trick a bit in my base template. The following code sits right after the front matter in my base layout and solves that problem.

{# Override front matter if the current page is an article #}
{% if article.title %}
    {% set title = article.title %}
{% endif %}

{% if article.metaDescription %}
    {% set metaDescription = article.metaDescription %}
{% endif %}

{% if article.slug %}
    {% set metaSlug = '/articles/' + article.slug+ '/' %}
{% endif %}

Triggering New Netlify Builds From WordPress

That's how the pages of the "article" part of this site are generated. Within my WordPress I'm currently using the plugin Deploy with NetlifyPress to trigger a new build of my page when I change one of the posts in my custom post type. There are several other plugins out there, but I like that I can specify which Custom Post Type should trigger a build. And that's it. If you have any questions or ideas: Feel free to contact me.

41 Webmentions

  1. Someone mentioned the article in this post.
  2. Avatar of Matt O'LearyMatt O'Leary liked this post on twitter.
  3. Someone mentioned the article in this post.
  4. Someone mentioned the article in this post.
  5. Avatar of Stephen HayStephen Hay replied to this post on twitter:
    Thanks for this. I fancy not using WP at all, but this is still useful to know!
  6. Avatar of yucelyucel liked this post on twitter.
  7. Avatar of Bejamas.ioBejamas.io mentioned the article in this post on twitter.
  8. Avatar of Eduardo UribeEduardo Uribe replied to this post on twitter:
    oooh... a little off-topic but I love that black-text/gray-background combination. 🔥
  9. Avatar of Eduardo UribeEduardo Uribe liked this post on twitter.
  10. Avatar of Sush KellySush Kelly liked this post on twitter.
  11. Avatar of Sush KellySush Kelly replied to this post on twitter:
    Yassss! Goes to put the kettle on
  12. Avatar of Andy BellAndy Bell liked this post on twitter.
  13. Avatar of Søren Birkemeyer 🦊Søren Birkemeyer 🦊 liked this post on twitter.
  14. Avatar of Christopher GoldbergChristopher Goldberg mentioned the article in this post on twitter.
  15. Avatar of hisophiabrandthisophiabrandt liked this post on twitter.
  16. Avatar of David Hund ✌David Hund ✌ liked this post on twitter.
  17. Avatar of Eric ValoisEric Valois liked this post on twitter.
  18. Avatar of Jens GrochtdreisJens Grochtdreis liked this post on twitter.
  19. Avatar of WebworkerWebworker liked this post on twitter.
  20. Avatar of RodolpheRodolphe liked this post on twitter.
  21. Avatar of Pat Ramsey “Scream inside your heart”Pat Ramsey “Scream inside your heart” liked this post on twitter.
  22. Avatar of Carles MuiñosCarles Muiños liked this post on twitter.
  23. Avatar of bertrandkellerbertrandkeller liked this post on twitter.
  24. Avatar of Florian WeilFlorian Weil reposted this post on twitter.
  25. Avatar of Sami KeijonenSami Keijonen reposted this post on twitter.
  26. Avatar of SamSam liked this post on twitter.
  27. Avatar of Taylor PageTaylor Page liked this post on twitter.
  28. Avatar of Shane RobinsonShane Robinson liked this post on twitter.
  29. Avatar of Taylor PageTaylor Page reposted this post on twitter.
  30. Avatar of Reginald HuntReginald Hunt liked this post on twitter.
  31. Avatar of Gaël PoupardGaël Poupard liked this post on twitter.
  32. Avatar of EleventyEleventy reposted this post on twitter.
  33. Avatar of Chris CoyierChris Coyier liked this post on twitter.
  34. Avatar of Tristan GibbsTristan Gibbs liked this post on twitter.
  35. Avatar of Jay HughesJay Hughes liked this post on twitter.
  36. Avatar of EleventyEleventy liked this post on twitter.
  37. Avatar of AlexaAlexa liked this post on twitter.
  38. Avatar of Stephanie EcklesStephanie Eckles liked this post on twitter.
  39. Avatar of Søren Birkemeyer 🦊Søren Birkemeyer 🦊 liked this post on twitter.
  40. Avatar of Søren Birkemeyer 🦊Søren Birkemeyer 🦊 reposted this post on twitter.
  41. Avatar of Marc GörtzMarc Görtz liked this post on twitter.

Other articles I've written recently