Nested taxonomy terms

I’m working on a wikipedia-based site and I needed to replicate its nested categories system, like in here Category:Philosophy - Wikipedia.

It isn’t something Hugo or any other SSG provides out of the box. I did some research and found some posts here asking about related things, but none provided a solution.

I thought it was not possible to do something like that in the current build.

Fast forward a few days, I had an epiphany, read the taxonomies documentation and set out to code. After some tests it seems to be functional and I decided to share with the community. Here’s how it’s built.

Ps: it nests taxonomy terms, and not the taxonomies themselves. This was something I took a while to realize I needed to do.

Create a data file with an entry for the taxonomies (as many as you need), and under it map the tree of relations between terms, like this:
data/terms_trees.yaml

categories:
  termA:
    termA1:
  termB:
    termB2:
      termB11:
        termB111:
    termB1:
      termB21:
      termB22:
      aeste:

Important!
The taxonomy entry has to be its exact plural, and each term must have a “:” after it’s name to indicate it’s a map, otherwise the code will break.

And if you put the data file inside a folder in your data folder you will have to modify the code accordingly to point it to the right place, which shouldn’t be hard.

Then create a partial to work like sort of a recursive function.
partials/functions/list-subterms.html

{{ $target := .target }}
{{ $page := .page }}
{{ $singular := index .page.Data "Singular" }}
{{ $plural := index .page.Data "Plural" }}
{{ $existingterms := .existingterms }}
{{ $scratch := newScratch }}

{{ range $key, $item := .start }}
{{ if eq $key $target }}
{{ $scratch.Add "targeted branch" $item }}
{{ break }}
{{ end }}
{{- partial "functions/list-subterms.html" (dict "start" $item "target" $target "page" $page "existingterms"
$existingterms) -}}
{{ end }}

{{ $targetedbranch := $scratch.Get "targeted branch" }}
{{ with $targetedbranch }}
{{ range $key, $item := . }}
{{ range $existingterms }}
{{ if eq (lower $key) . }}
{{ $scratch.Add "final list" (slice (site.GetPage (printf "/%s/%s" $plural ($key | urlize)))) }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}

{{- $final := ($scratch.Get "final list") -}}

<!-- create a list with all uppercase letters -->
{{ $letters := split "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "" }}

<!-- range all pages sorted by their title -->
{{ range $index, $item := $final.ByTitle }}
    <!-- get the first character of each title. Assumes that the title is never empty! -->
    {{ $firstChar := substr $item.Title 0 1 | upper }}
    <!-- in case $firstChar is a letter -->
    {{ if $firstChar | in $letters }}
        <!-- get the current letter -->
        {{ $curLetter := $scratch.Get "curLetter" }}
        <!-- if $curLetter isn't set or the letter has changed -->
        {{ if ne $firstChar $curLetter }}
            <!-- update the current letter and print it -->
            {{ $scratch.Set "curLetter" $firstChar }}

            {{ if ne $index 0 }}</div>{{ end }}
            <div class="subcategory-group">
            <h3>{{ $firstChar }}</h3>
        {{ end }}
        <ul><li><a href="{{ $item.Permalink }}">{{ $item.Title }}</a></li></ul>
    {{ end }}
{{ end }}
</div>

It’s currently set up to separate the terms by initial letter, with I achieved with a very handy code I found in this same forum but I don’t remember the original author (sorry).

Then in your _default/list.html or in whatever list template you want place this little ugly monster of a code:

{{ if eq .Page.Kind "term" }}
{{ $thistax := index $.Page.Data "Plural"}}
{{ $thisterm := index $.Page.Data "Term"}}
{{ range $index, $item := .Site.Taxonomies }}
{{ if eq $index $thistax }}
{{ range $i, $j := $item }}
{{ $.Scratch.Add "existingterms" (slice $i) }}
{{ end }}
{{ end }}
{{ end }}
{{ range $datatax, $map := .Site.Data.terms_trees }}
{{ if eq $datatax $thistax }}
<h2>Sub{{ index $.Page.Data "Plural" }}</h2>
<p>This {{ index $.Page.Data "Singular" }} has the following {{ len . }} sub{{ index $.Page.Data "Plural" }}.</p>
{{- partial "functions/list-subterms.html" (dict "start" $map "target" $thisterm "page" $.Page "existingterms"
($.Scratch.Get "existingterms") ) -}}

{{ end }}
{{ end }}
{{ end }}

And that’s it! Please pardon my spaghetti code, I’m just an amateur…

Unfortunately my site isn’t ready to be put online, but if the forum allows me to edit the post later I will provide a working example here.

3 Likes

Pretty cool, i hope it works out well!

Curious as to why you used Scratch variables instead of regular ones

2 Likes

No particular reason I can remember right now, I think it just came out this way

I guess I could have used regular ones too

1 Like

@thm_pedro This is fascinating, although I understand very little of it currently.

I am looking to create a simple business directory on one of my hugo sites and I would like the user to be able to select business services by city.

For example, selecting businesses offering “tree surgery” services in Birmingham while also being able to select tree surgery in Nottingham.

  • Birmingham

    • tree surgery
    • plumbing
    • gardening
  • Nottingham

    • tree surgery
    • plumbing
    • gardening
  • Manchester

    • tree surgery
    • plumbing
    • gardening

Would you say your method is suitable for this kind of structure or is there another way to acheive this?

If i understand what you mean what you want is different, maybe you will have to grab values that are in the intersection of terms, say, Birmingham and plumbing.

I would start building a list of cities and a list of services, then intersect them and display

That’s useful, thank you. I haven’t come across that function, but I will look into how it works and what it does.

The official documentation on intersection seems a bit light, are you able to recommend any other resources?

Apologies for my ignorance, I am a novice.

I don’t know resources other than this forum, sorry

Hugo docs requise some of trial and error, but that’s the fun of it