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.