joshtronic

Posted in Software Development tagged JavaScript

Category and Tag Pages with Eleventy

After switching from WordPress to Eleventy (11ty) earlier this year, I'm still playing catch up on a few things. One such thing, is getting category and tag pages setup again.

With Jekyll, I had setup a series of individual pages, one for each category and one for each tag. Quite a manual setup, but it worked well enough. Each page would filter on either category or tag, and was nicely paginated. All of this from a few lines of front matter.

I figured, I could pull this off in 11ty, but it took a bit of tinkering.

Categories in Eleventy

Out of the box (OOTB), 11ty can support categories, but tags are really the first class piece of meta. Some of the niceties you get for free are limited to tags, which means having to get a bit dirtier with the code if I wanted to support category pages.

It's in the back of my head that I should just move to tags exclusively, to play nice with 11ty's assumptions. With nearly 20 years of decently categorized content, I wasn't ready to make that jump.

Because I prefer consistent interfacing, I ended up rolling a solution for both categories and tags. Collections support tags, but that got kind of messy quick, due to the "all" tag, and other tags related to other collections I have setup.

Pagination in Eleventy

Pagination in 11ty is... weird.

I'm going to be honest with you guys, I'm still not sure I understand how that part even works. I read about "double pagination" and how it seems like you need to implement your own pagination if you want to do something like this with a collection:

/category/:category/:page

Feeling as if I had already spent way too much time on this, I made a concession. Instead of paginated results for each category and tag, I settled on a nice list of each post (just the title) in said meta group.

It's a bit of "retcon" but I kind of actually prefer this approach. It does create some longer pages, but it lets you see all of the content in a category or tag. I think I have enough categories and tags that no single one is really too long.

Eleventy "meta" collection

When I first started working on this, I used the OOTB support for tags, and then rolled my own collection for categories. This only got me so far, as there were extra tags that I needed to filter out, and I struggled with iterating through the array of objects with Liquid templates.

Take 2, I opted to create a single "meta" collection 11ty that would house arrays of categories and tags, and the posts, keyed by category and tag. The shape of the collection looks sorta like this:

{
  categories: string[];
  tags: string[];
  posts: {
    categories: { [category: string]: Post };
    tags: { [tag: string]: Post };
  }
}

This gives us a nice collection that can be used to generate category and tag lists, as well as the individual category and tag pages. You can define the collection in your eleventy.config.js file as such:

eleventyConfig.addCollection('meta', (collectionApi) => {
  const collection = { posts: { categories: {}, tags: {} } };
  const categories = new Set();
  const tags = new Set();
  const posts = collectionApi.getFilteredByGlob('./posts/**/*.md').reverse();
  posts.forEach((post) => {
    if (!collection.posts.categories[post.data.category]) {
      collection.posts.categories[post.data.category] = [];
    }
    collection.posts.categories[post.data.category].push(post);
    categories.add(post.data.category);
    if (post.data.tags) {
      post.data.tags.forEach((tag) => {
        if (!collection.posts.tags[tag]) {
          collection.posts.tags[tag] = [];
        }
        tags.add(tag);
        collection.posts.tags[tag].push(post);
      });
    }
  })
  collection.categories = [...categories].sort();
  collection.tags = [...tags].sort();
  return collection;
});

Pardon the lack of vertical spacing, still working through why syntax highlight loses indention when it hits blank lines

Category and tag lists

With our meta collection created, we can now create index pages to list out all of our categories and tags. You could do this on a single page, separate pages, or add the categories and/or tags to an existing page. I like having a separate index page for categories and tags, and I prefer to use Markdown over Nunjucks as much as possible.

Categories list (categories.md)

---
layout: main
---

# Categories <ul> {%- for category in collections.meta.categories -%} <li><a href="/category/{{ category | slug }}">{{ category }}</a></li> {%- endfor -%} </ul>

Tags list (tags.md)

---
layout: main
---

# Tags <ul> {%- for tag in collections.meta.tags -%} <li><a href="/tag/{{ tag | slug }}">{{ tag }}</a></li> {%- endfor -%} </ul>

Category and tag pages

Now that we have our category and tag indexes created, we can move into the individual category and tag pages. Unlike how I had things setup in Jekyll, in 11ty I'm able to create a single page that generates a page for each category and tag.

Definitely a preferred setup, as I can add new categories and tags without needing to worry about setting up pages for them. This will come up in really handy if the day comes that I abandon categories and fully lean into tags.

Category pages (category.md)

---
layout: main
pagination:
  data: collections.meta.posts.categories
  size: 1
  alias: category
permalink: /category/{{ category | slug }}/
---

# Category: {{ category }} <ul> {%- for post in collections.meta.posts.categories[category] | reverse -%} <li><a href="{{ post.url }}">{{ post.data.title }}</a></li> {%- endfor -%} </ul>

Tag pages (tag.md)

---
layout: main
pagination:
  data: collections.meta.posts.tags
  size: 1
  alias: tag
permalink: /tag/{{ tag | slug }}/
---

# Tagged: {{ tag }} <ul> {% for post in collections[tag] | reverse %} <li><a href="{{ post.url }}">{{ post.data.title }}</a></li> {% endfor %} </ul>

Conclusion

As mentioned, this wasn't quite what I had set out to accomplish, but it does get the job done. I do like the more dynamic nature of things, and will probably try to figure out the quirks of 11ty's concept of "pagination" at a later date.

To see this all in action, you can check out my categories and tags pages, which link to the individual category and tag pages. Or just scroll to the top of this post and click on the category or tag.

AI Notes

I talk to robots... a lot. The more I talk to the robots, and embrace the advantages, the more I want to get my hands dirty and figure out things the old fashioned way. Tinkering, solving problems, getting frustrated and eventually creative with my solution.

With that, that's how I came up with this implementation. I did some reading of the fucking manual, and consulting other sites and blogs. While I can't guarantee that I didn't read any generated content, I can safely say I wrote the content on this post myself, and hand-crafted the code to implement this solution, like a good caveman.

No copilot, just me.