First of all, I do not have any solution for automatically creating tag pages automatically when they’re referenced. I’m not sure there’s any solution to that at all, but a shell script can trivially fill that gap. Once you accept that limitation the rest is fairly easy.

Suppose you want to include a list of tags to the top of each post, and you want those tags to link to a page which lists all posts which include that tag.

In my searches I found a handful of different blog posts documenting the same thing, and I think they all track back to this solution, which was posted on StackOverflow.

You’ll need a /_layouts/tagpage.html file with something like this in it:

---
layout: default
---
{%- assign page_slug = page.name | remove: '.md' -%}
<h1>Posts tagged "{{page_slug}}"</h1>

{{ content }}

{%- assign tagged = site.posts | where_exp: "item", "item.tags contains page_slug" -%}
<ul>
  {%- for post in tagged -%}
  <li><h3><a href="{{ post.url | relative_url }}">
      {{- post.title | escape -}}
  </a></h3></li>
  {%- endfor -%}
</ul>

You’ll probably want to take most of the content from another layouts file in order to get all the proper formatting metadata.

Ideally we would use page.slug directly, but it doesn’t seem to be available in the Github Pages version of Liquid, so we make our own.

With that layouts page in place, you can have that layout applied automatically to files in /tags/ by adding the following to /_config.yml:

defaults:
  -
    scope:
      path: "tags"
    values:
      layout: tagpage

Now all you need is an empty file in /tags/ and the list of posts with a tag matching its filename will be created automatically.

I’ll leave it up to your imagination how you script a solution to create empty files for new tags.

It doesn’t have to be an empty file, though. You can add front matter variables and additional content for the tag page as well. The body of the file gets inserted where {{ content }} appears in the _layout file.

For example, in my generation loop for tags pages I recurse one layer into the other tags pages and check to see if they’re tagged with the tag I’m rendering. I can tag number-theory with mathematics, so that tagging something number-theory causes it to also appear under mathematics. I also allow some tags to be marked hidden so that they’re not listed on the page that uses them.

Which brings us to the other thing promised. A header on each post saying which tags it includes. Somewhere in /_layouts/post.html, where it’s a good place to inject the list of tags, insert:

{%- if page.tags.size > 0 -%}
  [ {% include tag_list.html %} ]
{%- endif -%}

This calls out to another file because I use the same loop in a couple of places, so in /_includes/tag_list.html:

{%- assign tag_pages = site.pages | where: "layout", "tagpage" -%}
{%- assign separator = '' -%}
{%- for tag in page.tags -%}
  {%- assign tag_page_name = tag | append ".md" -%}
  {%- assign tag_page = tag_pages | where_exp: "item", "item.name == tag_page_name" | first -%}
  {%- unless tag_page.hidden -%}
    {{- separator -}}
    {%- if tag_page.url -%}
      <a href="{{tag_page.url | relative_url}}">{{tag_page.display_name | default: tag}}</a>
    {%- else -%}
      {{tag}}
    {%- endif -%}
    {%- assign separator = ' | ' -%}
  {%- endunless -%}
{%- endfor -%}

Again, we would prefer a test for item.slug == tag but that’s not necessarily available.

Here I make reference to front-matter variables called hidden and display_name. The former skips over generating output if the tag is marked hidden (if front matter includes hidden: true), and the latter replaces the tag name with a potentially more presentable display name; for example if it needs a space in it, or other problematic characters.

I also check tag_page.url to make sure it has one. If it doesn’t then the tag page probably hasn’t been created. This could be treated the same way as hidden, but I just render the text with no link.

The other thing you might want is a master index of tags. I haven’t bothered, myself, but there’s one in that StackOverflow link, above.

Recursive tag lists

To include other tag pages on a tag page, one can add tags: foo bar to a tag page itself, and in the _layouts/tagpage.html extend the search list from site.posts to site.pages concatenated with site.pages | where: "layout", "tagpage". This will create an entry for those tag pages alongside the posts.

The next step is (if you want to) to recurse into those sub-tags and list the additional posts which match. Remembering to avoid duplicate listings. My approach to this was to make a dedicated _include/tagpage_excerpt.html file, which behaved similarly to _layouts/tagpage.html but using a more terse layout.

TODO: more code