Jekyll Tag Index Plugin
6th September 2020
A simple Jekyll plugin for generating tag indices

I’m using Jekyll to generate this site. It does a great job transforming existing content, but in order to achieve the tag indices I needed it to create some pages without direct sources. There is already a Jekyll tagging plugin out there that sort of does what I want, but not quite. That, and my desire to completely understand every part of how this site works, led me to decide to write a plugin myself to handle index generation.

Jekyll is written in Ruby which I had never used before. Thankfully, it’s a very straight forward language and was easy to pick up. I spent a little time going through this TryRuby quick start tutorial and decided that was plenty for writing a plugin. Hubris is a pretty key part of being a programmer, after all ;)

Jekyll’s plugins are well structured and easy to understand, and what I wanted to do wasn’t exactly rocket science, so I just dove right in. After an evening of fiddling I had per-category tag indices generating and mostly styled. I’m calling that a win!

The plugin has three main parts. First up is a tag index page class, which is responsible for generating the basic page that represents each tag index. This is exactly the page that I would be creating as a post if I were making these indices by hand. Note that this is where we set up the front matter that drives the page.

class CategoryTagIndexPage < Page
    def initialize(site, base, dir, category, tag)
        @site = site
        @base = base
        @dir = dir 
        
        safe_tag = Utils.slugify(tag)  
        @name = "#{safe_tag}-index.html"

        template_dir = site.config['template_dir'] || '_templates'

        self.process(@name)
        self.read_yaml(File.join(base, template_dir), site.config['category_tag_index_template'])
        self.data['categories'] = [category]
        self.data['tag'] = tag

        self.data['title'] = "#{category} : #{tag}"
    end
end

Next is a class which generates a list of all the tags on posts in a given category, creates a new instance of the page class for each tag, and serialzes each of those into pages for the site.

class CategoryTagIndexGenerator < Generator
    safe true
    priority :low

    def get_category_tags(site, category)
        category_tags = Hash.new {0}
        site.categories[category].each do |post|
            post.data['tags'].each do |tag|
                category_tags[tag] += 1
            end
        end

        return category_tags
    end

    def generate(site)
        site.categories.each_key do |category|
            dir = File.join(category, site.config['category_tag_dir'])

            category_tags = get_category_tags(site, category)

            category_tags.each_key do |tag|
                site.pages << CategoryTagIndexPage.new(site, site.source, dir, category, tag)
            end
        end
    end
end

These genrated pages use a template file, which is just a chunk of Liquid (the template language that Jekyll uses) that generates a list of appropriately tagged posts and does one of two things:

  1. Include a file based upon the site configuration for the specified category
  2. If there isn’t an include file availble, just make a simple list of tags
---
layout: tag-index
---
{%- assign category = page.categories[0] -%}
{%- assign tagged_posts = site.categories[category] | where_tagged_with: page.tag -%}

{%- if site.data.sections[category]['tag-index-inc'] -%}
    {%- assign include_file = site.data.sections[category].tag-index-inc -%}
    {%- include {{ include_file }} tag=page.tag category=category tagged_posts=tagged_posts -%}
{%- else -%}
    <h1>{{category}} : {{page.tag}}</h1>

    <ul>
    {%- for post in tagged_posts -%}
        <li>
            <a href="{{ post.url }}">{{ post.title }}</a>
        </li>
    {%- endfor -%}
    </ul>
{%- endif -%}

Finally, the include is responsible for iterating through the supplied tagged posts and including the appropriate HTML for them. Below is the one for the Makes section of the site.

{%- include project-gallery-begin.html -%}
{%- for post in include.tagged_posts -%}
    {%- include project-gallery-entry.html post=post -%}
{%- endfor -%}
{%- include project-gallery-end.html -%}

This may seem like a lot of indirection, but it gives me the freedom to style the index page per category. That’s important to me as projects need one kind of formatting but other content, like blog posts (which I will one day include in this site) probably want something completely different.

I was a little worried when I started this project that I was going to end up falling down a deep rabbit hole of web programming (which is not something I’m terribly experienced at) but it turned out to be an easy, fun, and fairly satisfying one night project.