Sorting site tags in Jekyll

comments

Today I finally implemented my blog's tags page. The goal was to give two presentations of tags: one sorted by name and second sorted by posts count. That was very hard to implement in liquid only and finally I wrote custom liquid filter for this task. In this post I will go through steps I made to final solution.

In this post I show how to:

  • Sort tags by name alphabetically - liquid solution
  • Sort tags by posts count - liquid solution
  • Sort tags by name and posts count with custom liquid filter

Sort tags by name alphabetically - liquid solution

The first problem I encountered was standard liquid sort filter does sort strings by ASCII codes. So, if you have tags africa and Banana, liquid will sort them in order: [ 'Banana', 'africa' ]. That is not what I expected, and that need to be fixed.

You can get right sort order if you will downcase your tags. Unfortunately you cannot get posts from site.tags when tag name is downcased, therefore you need to save original tag name too.

I do the trick by concatenating all tags in form

<downcased-tag>#<original-tag>

and do sort them. Later, in for loop, I split items by # character to receive original tag name.

Lets see solution worked for me:

{% capture site_tags %}{% for tag in site.tags %}{{ tag | first | downcase }}#{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% assign tag_hashes = site_tags | split:',' | sort %}
<ul class="list-group">
{% for hash in tag_hashes %}
  {% assign keyValue = hash | split: '#' %}
  {% capture tag_word %}{{ keyValue[1] | strip_newlines }}{% endcapture %}
  <li class="list-group-item">
    <a href="/tags/{{ tag_word }}">
      {{ tag_word }}
      <span class="badge pull-right">{{ site.tags[tag_word].size }}</span>
    </a>
  </li>
{% endfor %}
</ul>

Sort tags by posts count - liquid solution

We can apply similar trick to do sort tags by posts count. To do that, we can concatenate tags in form

<tag-posts-count>#<downcased-tag>#<original-tag>

and then sort them.

{% capture site_tags %}{% for tag in site.tags %}{{ tag[1].size }}#{{ tag | first | downcase }}#{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% assign tag_hashes = site_tags | split:',' | sort %}
<ul class="list-group">
{% for hash in tag_hashes %}
  {% assign keyValue = hash | split: '#' %}
  {% capture tag_word %}{{ keyValue[2] | strip_newlines }}{% endcapture %}
  <li class="list-group-item">
    <a href="/tags/{{ tag_word }}">
      {{ tag_word }}
      <span class="badge pull-right">{{ site.tags[tag_word].size }}</span>
    </a>
  </li>
{% endfor %}
</ul>

One disadvantage of this approach is that tags with most posts count located below. One try to fix that issue is to use reversed keyword: {% for hash in tag_hashes reversed %}. But in that case tag names will be in descending order too.

The decision that comes to mind is to calculate maximum count of posts among all tags and concatenate tags in form

<maximum-count-of-posts-among-all-tags-minus-tag-posts-count>#<downcased-tag>#<original-tag>

sort them and then use reversed keyword in for loop. Unfortunately, I didn't found any way to calculate maximum count of posts in liquid and decided to write custom liquid filter.

Sort tags by name and posts count - custom liquid filter

With many attempts I finally complete with this filters:

module Jekyll
  module TagHelpersFilter
    def sort_tags_by_name(tags)
      return tags.map   { |k,v| [ k, v.size] }
                 .sort_by { |x| [ x[0].downcase ] }
    end

    def sort_tags_by_posts_count(tags)
      max_posts_count_among_all_tags = tags.max_by { |k,v| v.size }[1].size
      return tags.map   { |k,v| [ k, v.size ] }
                 .sort_by { |x| [ max_posts_count_among_all_tags - x[1], x[0].downcase ] }
    end
  end
end

Liquid::Template.register_filter(Jekyll::TagHelpersFilter)

Both filters returns sorted array of items in form [ tag-name, posts-count ].

For tags sorted by name I use template:

<ul class="list-group">
{% assign sorted_tags_by_name = site.tags | sort_tags_by_name %}
{% for tag in sorted_tags_by_name %}
  <li class="list-group-item">
    <a href="/tags/{{ tag[0] | prepend: site.baseurl }}">
      {{ tag[0] }}
      <span class="badge pull-right">{{ tag[1] }}</span>
    </a>
  </li>
{% endfor %}
</ul>

Similarly, for tags sorted by posts count I use template:

<ul class="list-group">
{% assign sorted_tags_by_posts_count = site.tags | sort_tags_by_posts_count %}
{% for tag in sorted_tags_by_posts_count %}
  <li class="list-group-item">
    <a href="/tags/{{ tag[0] | prepend: site.baseurl }}">
      {{ tag[0] }}
      <span class="badge pull-right">{{ tag[1] }}</span>
    </a>
  </li>
{% endfor %}
</ul>

Conclusion

That's it! Now I have simple solution for sorted tags lists.

For future work I'd like to polish look of lists with bootstrap's tabs.

Enjoy!

Comments