Back to all articles

Managing tags in Jekyll blog easily

To categorize your posts in Jekyll you can use categories, tags or both of them. In our blog, we did the latter, but it was a bit chaotic. I decided to clean this up, get rid of categories completely and use tags in a more convenient way.

My goal was to:

  • make it easy to assign tags to posts
  • display tags in post details
  • be able to click any of them and see all posts with this tag
  • display a list of all tags used in the blog

In theory, all of it is pretty easy to do.

To assign tags to a post, just put their names, space separated (or as a YAML array if you prefer this way), in the front matter:

tags: tech jekyll blog ruby
---

To display them, just use post.tags variable:

{% for tag in post.tags %}
  {% assign tag_slug = tag | slugify: "raw" %}
  <a class="tag-link"
    href={{ site.baseurl | append: "/tags/" | append: tag_slug | append: "/" }}
    rel="category tag">
    #{{ tag }}
  </a>
{% endfor %}

To create a tag page, which will display all posts with this tag, add tags collection in the _config.yml:

collections:
  tags:
    output: true
    permalink: tags/:path/

and add a layout:

---
layout: default
---

<div class="snippets">
  <h1 class="snippets-heading">Articles tagged with "{{ page.tag-name }}"</h1>

  {% for post in site.posts %}
    {% if post.tags contains page.tag-name %}
      {% include snippet.html %}
    {% endif %}
  {% endfor %}
</div>

This assumes that you have _tags directory and files for every tag you want to use there (e.g. jekyll.md) with following content:

tag-name: jekyll
---

Maybe it’s easy, but not really convenient - I wanted to be able just to add a tag to the front matter and have it working already, without a need to add an additional file to the _tags directory.

I discovered that there is a mechanism called hooks which I can use to achieve this goal.

I’ve written this simple hook which you can place in the _plugins directory:

Jekyll::Hooks.register :posts, :post_write do |post|
  all_existing_tags = Dir.entries("_tags")
    .map { |t| t.match(/(.*).md/) }
    .compact.map { |m| m[1] }

  tags = post['tags'].reject { |t| t.empty? }
  tags.each do |tag|
    generate_tag_file(tag) if !all_existing_tags.include?(tag)
  end
end

def generate_tag_file(tag)
  File.open("_tags/#{tag}.md", "wb") do |file|
    file << "---\ntag-name: #{tag}\n---\n"
  end
end

And voila! - I don’t need to bother to create tag files manually anymore!

The last thing then - I wanted to display all the tags used in the blog. I could easily display all the tags - using site.tags variable - but I didn’t want to include tags that were used in the past, but are not assigned to any post right now. Also, I thought it would be nice to display post count for each tag.

It turned out that you can write a simple plugin for that:

module AllTagsFilter
  include Liquid::StandardFilters

  def all_tags(posts)
    counts = {}

    posts.each do |post|
      post['tags'].each do |tag|
        if counts[tag]
          counts[tag] += 1
        else
          counts[tag] = 1
        end
      end
    end

    tags = counts.keys
    tags.reject { |t| t.empty? }
      .map { |tag| { 'name' => tag, 'count' => counts[tag] } }
      .sort { |tag1, tag2| tag2['count'] <=> tag1['count'] }
  end
end

Liquid::Template.register_filter(AllTagsFilter)

And use it like that in a template:

<div class="all-tags">
  All tags:
  <ul>
    {% assign tags = site.posts | all_tags %}
    {% for tag in tags %}
      {% assign tag_slug = tag['name'] | slugify: "raw" %}
      <li>
        <a class="tag-link"
          href={{ site.baseurl | append: "/tags/" | append: tag_slug | append: "/" }}
          rel="category tag">
          #{{ tag['name'] }} ({{ tag['count'] }})
        </a>
      </li>
    {% endfor %}
  </ul>
</div>

That’s it! I hope you find it useful.

Share this article: