New site part 5 - post categories

Part of a series about setting up the new 11ty version of this site

One of the most common problems encountered when moving from WordPress to 11ty — apparently — is tagging posts, not least because 11ty already has the concept of tags.

There are some solutions out there but they all seemed fairly complicated, so I've written my own. I've added the concept of categories for posts, which are generated at build time.

We have to make an edit in the config file to add a new collection called categories:

// eleventy.js - a new require and a new collection

const { generateCategoriesCollection } = require('./utils/categories');

module.exports = function(eleventyConfig) {

    ...

    // add the categories collection
    eleventyConfig.addCollection('categories', (collection => {
        const categoriesCollection = generateCategoriesCollection(collection);
        return categoriesCollection;
    }))

    ...
}

And then we need to create the generateCategoriesCollection() function:

The process is:

  1. Generate an array of the unique category names
  2. Use that array to generate an array of category objects, which have a title (the category name) and a posts property which is an array of all the posts tagged with that category
// src/utils/categories.js

const generateCategoriesCollection = (collection) => {

    // get all the posts ...
    const allPosts = collection.getFilteredByTag("post");

    // create an empty array for the category name strings
    let categoryNames = [];

    // run through all the posts, get category names, turn to lower text
    // and add them to the array
    for (let post of allPosts) {
        const catNames = post.data.categories;

        if (catNames && catNames.length > 0) {
            for (let i = 0; i < catNames.length; i++) {
                categoryNames.push(catNames[i].toLowerCase())
            }
        }
    }

    // generate an array from a set of the array (needlessly
    // complicated way to get unique values only)
    categoryNames = Array.from(new Set(categoryNames)).sort();

    // create an empty array for the category objects
    const categories = [];

    // now we have an array of unique category names, cycle through
    // them to make category objects

    for (let categoryName of categoryNames) {
        
        // first make a new object with the property title
        const category = {
            title: categoryName
        }

        // generate an array of all the posts that are 'tagged'
        // with this category
        const posts = allPosts.filter(post => {
            // get the categories from that post
            let catNames = post.data.categories;
            // if there are categories ...
            if (catNames && catNames.length > 0) {
                // map categories to lower-case to ensure no clashes
                catNames = catNames.map(item => item.toLowerCase());
                // if our category is in the categories, true; otherwise false;
                if(catNames.indexOf(categoryName) >= 0) {
                    return true;
                } else {
                    return false;
                }

            // no categories, so post can't be in THIS category
            } else {
                return false;
            }
        });

        // add the array of posts (reversed date order) as a property
        // of the category object ...
        category.posts = posts.reverse();

        // ... and add the new object to the array of category objects
        categories.push(category)
    }

    return categories;
}

Using categories

We now have a categories collection which is available throughout our 11ty project. This is used in two places:

  • the 'tag cloud' (more on that another time); and
  • category pages

I wanted to generate a page for every category with a list of all the posts that are linked to that category. To do that, I added a file src/pages/categories.njk which paginates over the categories collection:

// src/pages/categories.njk - pt1

// Paginating with size 1 means that each page will cover one category;
// I've aliased collections.categories as 'tag' (just to confuse things).
// Each page gets one category object, aliased as 'tag', which has a
// title (string) and a posts (array of posts)
---
title: "Categories"
layout: layouts/pages.njk
pagination:
    data: collections.categories
    size: 1
    alias: tag
permalink: "/tags/{{ tag.title }}/index.html"
---

// A header with the category name
<h1 class="as-h2 page-header">
    Tag: <span class="highlight">{{ tag.title }}</span>
</h1>

<br/>

// Iterate over each post in the posts array however you want
{%- for post in tag.posts -%}
    
    // do something with the post ...

{%- endfor -%}