Creating a tag "cloud" by category - or how to work with contexts, the dot .Scratch and a dollar!

My issue

I wanted to include a tag list on my category pages that only shows the tags that are actually present in [pages in] the category shown.

I found a few topics, but no real answers. In the end, it wasn’t so hard, but as a new Hugo user, dealing with the context shift is a bit of a learning curve. It took me a while to figure out why my tags showed inside my range, but not outside.

TLDR;

<!-- Get the tags for all the pages the current context -->
{{ $.Scratch.Set "tags" (slice) }}
{{ range .Data.Pages }}
  {{ range .Params.tags }}
    {{ $.Scratch.Add "tags" . }}
  {{end}}
{{end}}

<!-- Get all tags and show only the ones we grabbed in the step above -->
{{ range .Site.Taxonomies.tags.ByCount }}
  {{ if in ($.Scratch.Get "tags" |sort |uniq) .Name }}
    <a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a> {{ .Count }}
  {{ end }}
{{ end }}

How I got here

We need to deal with Hugo’s context shift. I found this article very helpful in explaining what happens, but the author mostly talks about passing variables down, and I wanted my tags to go up!

.Scratch

In the Hugo docs, .Scratch is described as:

.Scratch is available as a Page method or a Shortcode method and attaches the “scratched” data to the given page. Either a Page or a Shortcode context is required to use .Scratch .

Cool! So .Scratch attaches to the page I’m working on, and I can now update .Scratch, and it will work! No?
Ehhh, no. Actually, it doesn’t work that way. It took me quite a bit longer than I care to admit that “page” refers to something in the current context than and not the Category page that will eventually get rendered by Hugo. So if I call .Scratch as the first thing, it’s at the root of my page. If I call it inside range .Data.Pages, .Scratch will live there, and f I call it in the second range, it will live there. And for every time range loops over an item, I will get a new .Scratch. Empty. Once that sank in, I understood why my slice ended up empty, and the solution wasn’t too hard to find.

Just give it a dollar!

In the article I mentioned above, there’s a section about the top level context, and how it’s attached to $. So I added a dollar sign in front of my .Scratch, like so: $.Scratch, and lo and behold, it got filled round after round while range looped over all the pages in my category. Hurray!

Getting the links to tags

This was easy. I started by assembling all the tags in the site (range .Site.Taxonomies.tags.ByCount). First I thought it was a good idea to get their names and intersect those with the list I had in my .Scratch, but then I realised I still had to look up the page objects in .Site.Taxonomies.tags.ByCount to get the links to the tags, and was doing the same thing twice. All I needed was to check if the objects name was in my scratched list, and get the title and link. the in function did the trick.

And there it is, a tag list for the current category. A lot of learning went into those few lines of code.

Afterthoughts

Pages and dollars

I was seriously thrown off by the meaning of Page and Pages. Just like the dot, their meaning changes with the context. And then what you’re working on is a page too, so that was seriously confusing. Finding that explanation of the $ really helped a lot. I’m sure it’s in the docs somewhere, and once you know it, it’s unlikely you’ll ever forget, but for newbies like me it isn’t an obvious thing.
Edit: found it, it’s in the Templating Introduction.

You actually don’t need .Scratch (or $.Scratch) if you use a recent version of Hugo

Hugo now supports $variables in templates, so you could use those as well. However, I think using .Scratch and $.Scratch isn’t a bad thing. Having to add that dollar or not really makes you think about the scope, and also gives an immediate visual clue. I’m a sysadmin and cloud engineer and have used a lot of Bash, zsh and Python. Hugo scoping is a bit different, and that dollar sure helps!

2 Likes

Well duh!
I just realised .Count returns the total number of occurrences of a tag in the whole site, not just in the context we call this.

I don’t display the count, I only use it to sort the list of tags. But if you want to display the count for just the context you call this snippet in, you have to tally the number of tags and build a frequency map.

The code then becomes:

{{ $.Scratch.Set "tags" (slice) }}
{{ range .Data.Pages }}
  {{ range .Params.tags }}
    {{ $.Scratch.Add "tags" . }}
  {{end}}
{{end}}

<!-- Make a frequency map of the tags in this context -->
{{ $.Scratch.Set "freq" dict }}
{{ range $.Scratch.Get "tags" }}
  {{ $.Scratch.SetInMap "freq" . 0}}
{{ end }}
{{ range $.Scratch.Get "tags" }}
  {{ $.Scratch.SetInMap "freq" . (add (index ($.Scratch.Get "freq") .) 1)}}
{{ end }}

<!-- Get all tags and show only if in current context -->
{{ range .Site.Taxonomies.tags.ByCount }}
  {{ if in ($.Scratch.Get "tags" |uniq) .Name }}
    <li>
      <a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a> ({{ index ($.Scratch.Get "freq") .Name }}/{{ .Count }})
    </li>
  {{ end }}
{{ end }}

I had a similar problem to collect “SVG icon usage” and switched fro scratch to store.
Posted it here

HTH

2 Likes