Alright, let’s talk about rendering all your terms on a single taxonomy list page. This is Hugo’s way of giving you a directory of all the tags, categories, or any custom taxonomy you’ve dreamed up. It’s a simple concept, but as usual, the devil is in the details, and Hugo’s approach here is… well, let’s call it idiosyncratic.

The first thing you need to know is that Hugo doesn’t just automatically create a page for this. You have to tell it you want one. You do this by creating a template file with a very specific name. The format is layouts/[TAXONOMY]/list.html. So, for your garden-variety tags, you’d create layouts/tags/list.html. For a custom taxonomy like manufacturers, it would be layouts/manufacturers/list.html.

The Core Logic: Ranging Through .Data.Terms

Inside that template, the magic happens thanks to the .Data.Terms variable. This is a map where the keys are the term names (e.g., “javascript”, “golang”) and the values are page collections for each term. To list them all, you range through this map.

{{ define "main" }}
  <article class="page-width">
    <h1>All Tags</h1>
    <ul>
      {{ range .Data.Terms.Alphabetical }}
        <li>
          <a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a>
          <span class="count">({{ .Count }})</span>
        </li>
      {{ end }}
    </ul>
  </article>
{{ end }}

Now, why .Data.Terms.Alphabetical instead of just .Data.Terms? Because .Data.Terms gives you a map, and ranging over a map in Go is not ordered. Your list would be in random order every time you rebuilt the site, which is a fantastic way to infuriate anyone trying to find anything. .Data.Terms.Alphabetical returns a slice of term objects, sorted by name, which is almost always what you want. Each item in that slice has a .Page (the term’s page object, so you can get its .Title and .Permalink) and a .Count (the number of pages associated with that term).

When Alphabetical Isn’t What You Want

Sometimes you want the biggest hitters at the top. For that, you can use .Data.Terms.ByCount. This sorts the terms by the number of pages they’re associated with, descending. It’s perfect for a “top tags” cloud.

{{ range .Data.Terms.ByCount }}
  <li>
    <a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a>
    <span class="count">({{ .Count }})</span>
  </li>
{{ end }}

A word of caution: .ByCount sorts ascending by the term name if two terms have the same count. It’s a bit of a quirk. If you need absolute control—like always breaking ties by most recent post—you’ll have to sort it yourself with sort or collections.Sort, which is a whole other can of worms.

Making a Simple Tag Cloud

A list is fine, but a tag cloud is more visually interesting. The classic cloud varies the font size based on the popularity (count) of the term. This requires a little CSS, but the Hugo template part is straightforward. We calculate a size, usually based on a logarithmic scale to prevent one massively popular tag from dominating.

<div class="tag-cloud">
  {{ $maxSize := 2.0 }} <!-- em -->
  {{ $minSize := 0.8 }} <!-- em -->
  {{ $maxCount := float (index .Data.Terms.ByCount 0).Count }}
  {{ $minCount := float (index .Data.Terms.ByCount.Reverse 0).Count }}

  {{ range .Data.Terms.Alphabetical }}
    {{ $size := (add $minSize (div (mul (sub (float .Count) $minCount) (sub $maxSize $minSize)) (sub $maxCount $minCount)) ) }}
    <a href="{{ .Page.Permalink }}" style="font-size: {{ $size }}em;">
      {{ .Page.Title }} <span class="count">({{ .Count }})</span>
    </a>
  {{ end }}
</div>

This isn’t the prettiest code, I know. Hugo’s math functions are… functional. We’re calculating a size between $minSize and $maxSize proportional to where the term’s count falls between the $minCount and $maxCount. It’s a linear progression, which is simple, but a log scale often looks better. For that, you’d need a log function, which Hugo doesn’t have built-in, so you might need a custom function or a simpler method.

The Empty Taxonomy Pitfall

Here’s a fun edge case: what if a term exists but has zero pages assigned to it? Maybe you typo’d a tag and then corrected it. That original, unused term will still show up in .Data.Terms because it exists in your front matter history. It will have a .Count of 0. You might want to filter these out unless you’re a completionist.

{{ range where .Data.Terms.Alphabetical "Count" ">" 0 }}
  <!-- ... -->
{{ end }}

This is the kind of thing that makes you scratch your head. Why would I want to link to a page that proudly displays zero content? It’s like a restaurant with a sign that says “No Food Here.” I usually filter these out.