Customizing Tags

13 Jan 2021 5:32 PM    the blog jekyll
convert to local time zone


Today we’re taking a look at the tags on posts, and allowing me to customize them. The one specific goal we have in mind is making language tags that match the language color on GitHub and on the projects page.

To start, let’s take a look at the post layout before this change (on GitHub):

---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
      {% comment %}{% unless forloop.last %}, {% endunless %}{% endcomment %}
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>

I can’t highlight all the langauges appearing in this excerpt (for some reason), so I’ll highlight different bits. Note the HTML layout:

---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
      {% comment %}{% unless forloop.last %}, {% endunless %}{% endcomment %}
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>

post-main-content provides padding for the post div. The h1 and p are the title and date, with Bootstrap classes to put them closer together. We have the Liquid loop for the tags, and then after closing the <p> tag, we have a styled <hr> that separates the title & tags from the content.

If we take a look at the Liquid-highlighted version:

---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
      {% comment %}{% unless forloop.last %}, {% endunless %}{% endcomment %}
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>

we see where the variables are inserted, and a few things worth noting specifically. First, note the {% if page.tags[0] %}. This makes sure that we don’t produce anything if the post has no tags. Because we’re reading from YAML, this is apparently the best way to check if there are any tags. We add &nbsp;s for spacing, and then loop through the tags, linking each one to its own URL (that goes nowhere) and applying the badge-info CSS class. That’s what provides the teal coloring.

You may also notice the liquid-commented section that would put commas between the tags — that’s only there as a historical vestige, and now it’s on GitHub, so we’re removing it. Also, the {{ content }} is replaced with the content of each post.


So that’s what we had. Now, what do we want? I wanted to be able to customize the colors of some tags, while keeping a default color as well. My instinct to solve this was to create a Jekyll data file. So I created _data/tags.yml, with this content:

- name: swift
  class: badge-swift

The name is the tag name, and the class is the CSS class to apply. Because I’m using Bootstrap, if I apply badge badge-info to something it looks like this:

<span class="badge badge-info">example</span>

example

When I was working on the project list, I created a few badge-* classes of my own to mimic language colors on GitHub (as the Bootstrap badge-* classes are only coloring):

// language colors from https://github.com/ozh/github-colors
.badge-swift {
  background-color: #ffac45;
  color: #111111;
}
.badge-python {
  background-color: #3572A5;
  color: #ffffff;
}
.badge-java {
  background-color: #b07219;
  color: #ffffff;
}
.badge-kotlin {
  background-color: #F18E33;
  color: #ffffff;
}

So badge-swift actually already works. For example:

<span class="badge badge-swift">Swift</span>

Swift

We just need to take this data file and use it when “rendering” tags (I don’t actually do the rendering, that’s the user’s browser, but it feels like the right word).


One of the benefits of using Jekyll is that it’s very easy for me to run a local version of the site to test it. In my cloned version of the repository, I can run bundle exec jekyll serve,1 go to localhost:4000, e voilá. So I could stick various Liquid/Jekyll tags/filters/variables and see their output.

The first thing I did was stick {{ site.data.tags }} right after the two &nbsp;s, showing me what was in that variable. At this point in the process (where we’ve just created the tags data file), it outputs this:

{"name"=>"swift", "class"=>"badge-swift"}

Currently (i.e., as of the most recent update to the site), it outputs this:

{"name"=>"swift", "color"=>"F05138", "_name"=>"swift"}{"name"=>"jekyll", "color"=>"dc3545", "description"=>"the static site generator that powers this site. <a href=\"https://jekyllrb.com\">jekyllrb.com</a>", "_name"=>"jekyll"}{"name"=>"java", "color"=>"b07219", "_name"=>"java"}{"name"=>"rust", "color"=>"dea584", "_name"=>"rust"}{"name"=>"tex", "color"=>"3d6117", "_name"=>"tex"}{"name"=>"latex", "color"=>"3d6117", "_name"=>"latex"}{"name"=>"kotlin", "color"=>"a97bff", "_name"=>"kotlin"}{"name"=>"gradle", "color"=>"02303a", "_name"=>"gradle"}{"name"=>"python", "color"=>"3572A5", "_name"=>"python"}{"name"=>"nixos", "color"=>"7e7eff", "_name"=>"nixos"}{"name"=>"frc", "_name"=>"frc"}{"name"=>"the blog", "_name"=>"the blog"}{"name"=>"swift package manager", "_name"=>"swift package manager"}{"name"=>"macos", "displayname"=>"macOS", "description"=>"apple's desktop platform. my posts about it generally fall into one of two categories: tracking down bugs or doing complex configuration", "_name"=>"macos"}{"name"=>"neovim", "_name"=>"neovim"}{"name"=>"sudo", "_name"=>"sudo"}{"name"=>"shell", "color"=>"89e051", "_name"=>"shell"}{"name"=>"discord", "_name"=>"discord"}{"name"=>"rss", "_name"=>"rss"}{"name"=>"rssbot", "description"=>"a discord bot that watches RSS/Atom feeds. find it on GitHub <a href=\"https://github.com/Samasaur1/rssbot\">here</a>", "_name"=>"rssbot"}{"name"=>"refind", "_name"=>"refind"}{"name"=>"catppuccin", "_name"=>"catppuccin"}{"name"=>"svg", "_name"=>"svg"}{"name"=>"imagemagick", "_name"=>"imagemagick"}{"name"=>"ssh", "_name"=>"ssh"}{"name"=>"gnupg", "_name"=>"gnupg"}{"name"=>"tailscale", "_name"=>"tailscale"}{"name"=>"wireguard", "_name"=>"wireguard"}{"name"=>"vpn", "_name"=>"vpn"}{"name"=>"homebrew", "_name"=>"homebrew"}{"name"=>"macports", "_name"=>"macports"}{"name"=>"alacritty", "_name"=>"alacritty"}{"name"=>"css", "_name"=>"css"}{"name"=>"scss", "_name"=>"scss"}{"name"=>"nix", "color"=>"7e7eff", "description"=>"the Nix package manager", "_name"=>"nix"}{"name"=>"nix-darwin", "color"=>"7e7eff", "description"=>"declarative configuration of macOS using Nix", "_name"=>"nix-darwin"}

That shows just what we expected: a dictionary(/map/object). Keep in mind that this will be an array of dictionaries, but there’s only one element right now.

I knew that whatever we did, it would need to be inside the Liquid for loop, because we need to process for each tag on the article. My first thought was to check if site.data.tags contained tag (recall that tag is from the loop):

{% for tag in page.tags %}
  <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
{% endfor %}

Using the Liquid contains filter/tag (I really don’t know what they’re called) is false when used like so:

{% if site.data.tags contains tag %}true{% else %}false{% endif %}

even if we add the Swift tag, which makes sense if you think about it, because site.data.tags contains one object which has a name field matching our tag, but doesn’t match directly. I tried the following:

{% if site.data.tags | map: "name" contains tag %}true{% else %}false{% endif %}

This was true for both. I’m still not sure why this said true for both, but it doesn’t really matter to me — I just took a different approach.

Instead of using contains, I just looped over site.data.tags like so:

{% for tag in page.tags %}
  {% for site_tag in site.data.tags %}
    {% if site_tag.name == tag %}
      {{ site_tag.class }}
    {% endif %}
  {% endfor %}
  <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
{% endfor %}

Recall that the outer for loop already existed. This produces the CSS class we want, but not in the right place. I simply assigned it to a Liquid variable:

{% for tag in page.tags %}
  {% for site_tag in site.data.tags %}
    {% if site_tag.name == tag %}
      {% assign css-class = site_tag.class %}
    {% endif %}
  {% endfor %}
  {{ css-class }}
  <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
{% endfor %}

This produces the same output (because I’m outputting the variable), but I could easily put it in the <a> tag. Not yet, though — what happens for non-matching tags? I said I wanted a default.

My solution was just to predefine css-class, like so:

{% for tag in page.tags %}
  {% assign css-class = "badge-info" %}
  {% for site_tag in site.data.tags %}
    {% if site_tag.name == tag %}
      {% assign css-class = site_tag.class %}
    {% endif %}
  {% endfor %}
  {{ css-class }}
  <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge badge-info">{{ tag }}</a>
{% endfor %}

This now has a default but can be overwritten — just like we wanted. The only remaining step is to use it as a CSS class, by replacing the <a> tag line with this:

<a href="{{ "/tags/" | append: tag | relative_url }}" class="badge {{ css-class }}">{{ tag }}</a>

and remove the lone {{ css-class }} line, resulting in this _layouts/post.html file:

---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      {% assign css-class = "badge-info" %}
      {% for site_tag in site.data.tags %}
        {% if site_tag.name == tag %}
          {% assign css-class = site_tag.class %}
        {% endif %}
      {% endfor %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge {{ css-class }}">{{ tag }}</a>
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>
---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      {% assign css-class = "badge-info" %}
      {% for site_tag in site.data.tags %}
        {% if site_tag.name == tag %}
          {% assign css-class = site_tag.class %}
        {% endif %}
      {% endfor %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge {{ css-class }}">{{ tag }}</a>
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>
---
layout: default
---
<div id="post-main-content">
<h1 class="mb-0 pb-0">{{ page.title }}</h1>
<p class="mt-0 pt-0 text-muted">
{{ page.date | date_to_string }}
  {% if page.tags[0] %}
    &nbsp;&nbsp;
    {% for tag in page.tags %}
      {% assign css-class = "badge-info" %}
      {% for site_tag in site.data.tags %}
        {% if site_tag.name == tag %}
          {% assign css-class = site_tag.class %}
        {% endif %}
      {% endfor %}
      <a href="{{ "/tags/" | append: tag | relative_url }}" class="badge {{ css-class }}">{{ tag }}</a>
    {% endfor %}
  {% endif %}
</p>
<hr style="border: 1px solid;"/>
{{ content }}
</div>

or as an image, to see it all colored at once (I really need to figure that out): colorized image of the above code

I’ve been testing along the way with the swift tag on thie article, but it’s not about Swift, so I’m going to take it off. This does unfortunately mean that I won’t have an example as of now, but alas!

There are a few things still left to do:

Unitl next time!


  1. If you want it to be accessible on more than just that computer (e.g., for testing on mobile devices), I found this to work: bundle exec jekyll serve --host 0.0.0.0. You’d then go to [your computer's local IP address]:4000 on the mobile device. 


Respond to this