Nested Taxonomies with Hugo
I was looking into a WordPress to Hugo site migration recently. One problem I noticed was around Hugo's incomplete support for hierarchical taxonomies – something that WordPress offers with its notion of categories. I spent a while trying to adapt the built-in taxonomy conventions, but eventually I decided it wasn't practical. Instead, I switched to using regular content sections and some extra templating magic to support these more complex taxonomies. The following are general strategies that I found to work for me.
Starting with Content
The first type in this example is the taxonomy-equivalent type which I call a collection. To maintain the hierarchy, each collection must be its own section. For example, the following represents the Confections category (which itself is a subcategory of Sweets):
---
title: "Confections"
---
Desserts, candies, and sweet breads
The next type is a product, and these are regular pages which can include a list of the collections they belong to. For example:
---
title: "Schoggi Schokolade"
brand: "heli-susswaren-gmbh-co-kg"
collections:
- "sweets/confections"
- "sales/2020-11"
---
100 - 100 g pieces
Listing Pages
When it comes to rendering collections, I primarily use the section.html
template. One of the traditional views is to show all the products in the collection. For this, a straightforward where
function with an intersect
can be used:
<dt>Products</dt>
{{- range where .Site.Pages ".Params.collections" "intersect" ( slice ( .File.Path | strings.TrimPrefix ( printf "%s/" .Type ) | strings.TrimSuffix "/_index.md" ) ) }}
<dd><a href="{{ .Permalink }}">{{ .Title }}</a></dd>
The result for a collection with two products then looks something like:
<dt>Products</dt> <dd><a href="/product/schoggi-schokolade/">Schoggi Schokolade</a></dd> <dd><a href="/product/teatime-chocolate-biscuits/">Teatime Chocolate Biscuits</a></dd>
Listing Nested Pages
A more complicated view (which I was not able to reproduce with built-in Hugo taxonomies) was to list all items in this taxonomy collection and sub-collections. To support this, I created a template helper function to recursively capture all the sub-collections into a .Scratch
variable:
{{ define "_nested_sections_slice" -}}
{{ $.Scratch.Add "nested_sections_slice" ( slice ( .Section.File.Path | strings.TrimPrefix ( printf "%s/" .Section.Type ) | strings.TrimSuffix "/_index.md" ) ) -}}
{{ range .Section.Sections -}}
{{ template "_nested_sections_slice" ( dict "Scratch" $.Scratch "Section" . ) -}}
{{ end -}}
{{ end -}}
Then, after executing it, I use it with another where
/intersect
lookup (and I could add Paginate
for long lists):
<dt>Nested Products</dt>
{{- template "_nested_sections_slice" ( dict "Scratch" .Scratch "Section" . ) }}
{{- range where .Site.Pages ".Params.collections" "intersect" ( $.Scratch.Get "nested_sections_slice" ) }}
<dd><a href="{{ .Permalink }}">{{ .Title }}</a></dd>
{{- else }}
<dd>none</dd>
{{- end }}
The result for a collection containing two sub-collections with three total products then looks like:
<dt>Nested Products</dt> <dd><a href="/product/grandmas-boysenberry-spread/">Grandma's Boysenberry Spread</a></dd> <dd><a href="/product/schoggi-schokolade/">Schoggi Schokolade</a></dd> <dd><a href="/product/teatime-chocolate-biscuits/">Teatime Chocolate Biscuits</a></dd>
Page Navigation
For the product pages, a common view is to show breadcrumbs for the taxonomy collections it is in. To help with that, I created a template function that I can call with a collection to show the hierarchy:
{{ define "taxonomy-hierarchy" }}
{{- if and .Parent ( eq .Parent.Type .Type ) }}
{{- template "taxonomy-hierarchy" .Parent }} / {{ end -}}
<a href="{{ .Permalink }}">{{ .Title }}</a>
{{- end }}
Then, on the individual product pages, I can show a set of breadcrumbs for each collection. I use the .GetPage
function to load the collection before passing it to the template function (with errorf
helping to avoid frontmatter typos):
<dt>Collections</dt>
{{- range .Params.collections }}
{{- with $.Site.GetPage ( printf "collection/%s" . ) }}
<dd>{{ template "taxonomy-hierarchy" . }}</dd>
{{- else }}
{{ errorf "unable to find collection: %s" . }}
{{- end }}
{{- end }}
The result for a product in the Confections and On Sale collections then looks like:
<dt>Collections</dt> <dd><a href="/collection/sweets/">Sweets</a> / <a href="/collection/sweets/confections/">Confections</a></dd> <dd><a href="/collection/sales/">On Sale</a> / <a href="/collection/sales/2020-11/">This Week's Deals</a></dd>
Still, a taxonomy
One last thing: in order to avoid confusion of the collection/
root directory being a collection itself, I explicitly give it a type of taxonomy
. There's nothing magic about the name taxonomy
here – it is just another content type – but it does help simplify some of the earlier template functions and make it easier to have a different theme template at that path.
---
type: "taxonomy"
title: "Collections"
---
Note: the sample site for examples from this post is available if you want more details or to run hugo serve
yourself.