Tag aliases and term canonicalization when applying taxonomies

Problem statement

I want to implement two features for my tag = "tags" taxonomy, inspired by software like Hydrus Network or TagStudio:

  • Tag aliases, or canonical/sibling tags. For example, if I tag something as yugioh, yu-gi-oh, or ygo, loading the term list page for any of these will return content tagged with any of these. One of these can be declared canonical, e.g. yu-gi-oh, and in this case, yugioh and ygo should/could be normalized to yu-gi-oh. At the very least, /tags/yugioh/ should either return that tagged content, or otherwise redirect to /tags/yu-gi-oh/ instead.
  • Tag composition, or hierarchical/parent tags. For example, if I tag something as yugi muto, this logically implies the tag yu-gi-oh (duel monsters), which in turn logically implies the tag yu-gi-oh.

For the latter, I found Hierarchical Tags in Hugo which I intend to look at later, but for now I wanted to start with the former.

What I’ve tried

Preamble: content adapters and site data

I vaguely intend to do this with content adapters and some site data in data/tags/, since this would allow me to use that data for other things, and it would save me from polluting my content directory.

I take the following data from data/tags/yugioh.json:

{
	"@context": [
		"https://www.w3.org/ns/activitystreams",
		{
			"@base": "https://trwnh.com/tags/"
		}
	],
	"id": "yu-gi-oh",
	"name": "Yu-Gi-Oh!",
	"summary": "A media franchise including several series mostly revolving around the fictional card game of Duel Monsters.",
	"alsoKnownAs": ["yugioh", "yu-gi-oh!", "ygo"]
}

I use the following content/tags/_content.gotmpl file:

{{ $data := .Site.Data.tags }}

{{ range $data }}

	{{ $title := .name }}
	{{ $summary := .summary }}
	{{ $aliases := .alsoKnownAs }}

	{{ $page := dict
		"title" $title
		"summary" $summary
		"aliases" $aliases
		"path" .id
		"kind" "term"
	}}
	{{ $.AddPage $page }}

{{ end }}

By my understanding, this should be equivalent to creating a tag page at content/tags/yu-gi-oh/_index.md with the following frontmatter:

title = "Yu-Gi-Oh!"
summary = "A media franchise including several series mostly revolving around the fictional card game of Duel Monsters."
aliases = [
  "ygo",
  "yugioh",
  "yu-gi-oh!"
]

It is rendered by the template at layouts/_default/term.html:

{{ define "body" }}

{{ $tagSet := slice .Name | append .Aliases }}
{{ $pages := where .Site.RegularPages "Params.tags" "intersect" $tagSet }}

<body class="layout-_default-term">
	<main id="main">
		<h1>Tag title: {{.Title}}</h1>
		<p>Summary: {{.Summary}}</p>
		{{ range $pages }}
		<li><a href="{{ .Permalink }}">{{ .Permalink }}</a></li>
		{{ end }}
		{{ range .Aliases }}
		<p>{{.}}</p>
		{{ end }}
	</main>
</body>

{{ end }}

Using tag aliases

Given content/foo.md with the following frontmatter:

tags = ["ygo"]

Given content/bar.md with the following frontmatter:

tags = ["yugioh"]

The intended result is to:

  • have /tags/yu-gi-oh/ list both pages
  • have /tags/ygo/ list both pages, or redirect to /tags/yu-gi-oh/
  • have /tags/yugioh/ list both pages, or redirect to /tags/yu-gi-oh/
  • have /tags/yu-gi-oh!/ list both pages, or redirect to /tags/yu-gi-oh/

The actual result is:

  • /tags/yu-gi-oh/ lists both pages as expected
  • /tags/ygo/ only lists /foo/, and is not recognized as an alias (because it was overridden by the page generated from the frontmatter in content/foo.md)
  • /tags/yugioh/ only lists /bar/, and is not recognized as an alias (because it was overridden by the page generated from the frontmatter in content/bar.md)
  • /tags/yu-gi-oh!/ redirect to /tags/yu-gi-oh/ as expected

Where to go from here

The question is: am I on the wrong track, and if so, what is the right track? I understand that aliases will generally be overridden when actual content pages exist, but I want to bypass this behavior somehow.

You can’t.

Don’t use Hugo’s alias feature. Instead, in the term template, conditionally render a “refresh” meta tag if the current term is not canonical. You can do this using the base/block construct.

Keep in mind that Hugo’s related content feature will not work as desired with this (or any other) implementation of term aliases.