Hierarchical Tags in Hugo
Making your posts a dessert topping and a floor wax
Once again, if you’re not using Hugo to build a static site, your time is better spent elsewhere. Or maybe it’s not? I won’t pretend to know what’s going on with your life.
Hugo has a taxonomy system that is quite flexible. By default, the tag
field is already enabled, so you can quickly organize posts by adding tag values into the post frontmatter like:
---
title: "Edan - Beauty and the Beat"
tags:
- experimental-hip-hop
- psychedelic-rock
---
Now that post will show up in both /tags/experimental-hip-hop
and /tags/psychedelic-rock
. But what if we want it to also appear in tags/hip-hop
? One option is to create the following directory structure:
content/
├── tags/
├── hip-hop/
├── experimental-hip-hop/
├── _index.md
├── rock/
├── psychedelic-rock/
├── _index.md
But then, instead of tagging with just experimental-hip-hop
, you’ll have to use hip-hop/experimental-hip-hop
which is wordy and error-prone. Also, each tag will only be able to have a single parent.
Instead, we’ll try something different: we’ll add tags to the tag pages themselves. For example, create a file at content/tags/experimental-hip-hop/_index.md
and add the following frontmatter:
---
title: "Experimental Hip-Hop"
tags:
- hip-hop
---
This by itself won’t do much, we still won’t see Beauty and the Beat when visiting /tags/hip-hop
. The next step is to change the layout of our tag pages in order to also pull all pages from any child tags. Typically, the template for a tag page will look something like:
{{ range .Pages }}
<h2>{{ .Title }}</h2>
{{/* Post rendering logic goes here... */}}
{{ end }}
The .Pages
collection will only grab pages with the specific tag (e.g. hip-hop
), it does not include any child tags. We can adjust the code to include child tags by first adding a new partial
:
{{/*
Returns a slice of pages matching the given tag, including all pages from child tags.
Usage:
{{ $pages := partial "partials/get_descendant_tag_pages" (dict "Page" .) }}
*/}}
{{ define "partials/get_descendant_tag_pages" }}
{{ $matches := or .Matches slice }}
{{ $all_tag_pages = (where (where site.Pages ".Kind" "eq" "term") ".Type" "eq" "tags") }}
{{ range where $all_tag_pages ".Params.Tags" "intersect" (slice .Page.Data.Term) }}
{{ if not (in $matches .) }}
{{ $matches = $matches | append . }}
{{ $child_tag_pages := partial "partials/get_descendant_tag_pages" (dict "Page" . "Matches" $matches) }}
{{ $matches = $matches | union $child_tag_pages }}
{{ end }}
{{ end }}
{{ return $matches }}
{{ end }}
Now, update the template for the tag page to use the result of this partial instead of .Pages
:
{{ $pages := partial "partials/get_descendant_tag_pages" (dict "Page" .) }}
{{ range $pages }}
<h2>{{ .Title }}</h2>
{{/* Post rendering logic goes here... */}}
{{ end }}
Now, when you visit /tags/hip-hop
you’ll see Beauty and the Beat listed along with any other pages tagged with experimental-hip-hop
. One nice thing about this technique is that add multiple parent tags to a single tag page, so you can make nursery-rhymes
a child of both children
and death-metal
in order to confuse your headbanging friends.