Implementing Categories

I want there to be Categories in Eleventy to or­ga­nize ar­ti­cles into broad do­mains.1 The idea is that all ar­ti­cles about books will go into the Culture cat­e­gory, ar­ti­cles about tech go into the Tech cat­e­gory, and so on.

Tags, on the other hand, can be in any cat­e­gory. A weird book and some weird code would be in dif­fer­ent cat­e­gories, but they could both be tagged weird.

How we’ll use them

Let’s look at how we’ll use cat­e­gories:

Specifying the category

Use the category prop­erty like this to spec­ify the cat­e­gory an ar­ti­cle be­longs to:

---
date: 10/30/2018
title: Loomings
category: Tech
tags:
  - tools
  - git
  - eleventy
---

Using the category in a template

In a tem­plate, re­fer to the cat­e­gory prop­erty in the usual way:

<a href="/categories/Tech">Tech</a>

Working with articles in the same category

We can work with ar­ti­cles in the same cat­e­gory by cre­at­ing a categories col­lec­tion.2 To list all of the ar­ti­cles in the Tech cat­e­gory, you could do it this way:

<ul>
  {%- for article in collections.categories["Tech"] -%}
    <li>{{ article.data.title }}</li>
  {%- endfor -%}
</ul>

Just as collections is an ob­ject that has a prop­erty for each tag, so collections.categories has a prop­erty for each cat­e­gory. Each prop­erty refers to an ar­ray of ar­ti­cles. It looks some­thing like this:

collections: {
  all: [ items ],
  categories: {
    Culture: [ items ],
    Life: [ items ],
    Thinking: [ items ]
  }
}

The implementation

We want:

Creating a list of categories

To get the list of cat­e­gories, we it­er­ate over all of the ren­dered tem­plates. This code cre­ates a col­lec­tion called categoryList that con­tains the names of all the cat­e­gories.

getCatList = function(collection) {
  let catSet = new Set()

  collection.getAllSorted().forEach(item =>
        typeof item.data.category === "string"
    &&  catSet.add(item.data.category))

  return [...catSet]
}

eleventyConfig.addCollection("categoryList", getCatList)

Creating a list of articles for each category

To get lists of each ar­ti­cle in a cat­e­gory, we want to cre­ate an ob­ject that has a prop­erty for each cat­e­gory, and each prop­erty con­tains a list of ar­ti­cles of that cat­e­gory. In other words, we want to end up with an ob­ject that looks like this:

categories {
  Culture: [article_1, article_4],
  Tech: [article_3],
  Life: [article_1, article_3]
}

We can use the makeCategories() func­tion as a call­back to to addCollection() to cre­ate this ob­ject. We it­er­ate over each item that has a category property in its front mat­ter and add it to the list for that cat­e­gory:3

makeCategories = function(collection) {
  let categories = {}

    // Every rendered page

  collection.getAllSorted().forEach(item => {
    let category = item.data.category

      // Ignore the ones without a category

    if (typeof category !== "string")
      return

    if (Array.isArray(categories[category]))
        //  category array exists? Just push
      categories[category].push(item)
    else
        //  Otherwise create it and
        //  make `item` the first, uh, item.
      categories[category] = [item]
  })

  return categories
}

Since we want to call our col­lec­tion of cat­e­gories categories, we’d build it like this:

addCollection("categories", makeCategories)

So now we have a way to make a col­lec­tion that has col­lec­tions of its own. I use it to seg­re­gate my posts into unique buck­ets.

  1. That’s what I say now. The orig­i­nal im­pe­tus for this was to fig­ure out how col­lec­tions work. ↩︎

  2. You can use any name you like. I hap­pen to like categories”. ↩︎

  3. This if (Array.isArray(categories[category])) thing is re­ally stu­pid. Is­n’t there a way to push to an ar­ray, cre­at­ing it if it does­n’t ex­ist. ↩︎