This website uses tags, as provided by Hugo’s built-in taxonomy support. The site includes a page which lists my tags. The page includes an alphabetical listing of all the tags, grouped by initial letter:
I build this list in two steps:
Step 1: collect all the tags into a map. The keys are the first letters of the tag, and the value for each key is an array of all the tags starting with that letter.
Step 2: iterate over the map to write out each array of tags grouped by letter. The key becomes the heading of the group.
As with many things in Hugo, there is more than one way to do this. Here are two variations on one possible way:
Use a scratch variable to collect the data, then iterate over the backing map of the scratch.
Use a map instead of a scratch variable.
Here is the code which populates the newScratch variable:
|
|
The list of tags is accessed using .Data.Terms.Alphabetical
. You can see the structure of this object by using dump
:
|
|
You will see something like the following JSON for each dumped sub-object:
|
|
As well as the tag itself ("Name": "3d"
), there is an array of all of the pages which use that tag. We don’t use that array here - we only access the Name
of the tag.
NewScratch
is a collection of key-value pairs. It comes with various methods, including Get
, Set
and Add
. My code uses these methods to add new entries to the scratch, or to add a new tag to an existing entry.
So, for example, for the letter u
, we will end up with:
|
|
We check if the key already exists in our scratchpad. If it doesn’t, then the first entry is .Set
as an array, using slice
:
|
|
Otherwise, we .Add
to the existing entry:
|
|
As noted in the documentation:
If the first Add for a key is an array or slice, the following adds will be appended to that list.
This is why subsequent entries don’t need to be placed in an array.
Here is the code for step 2 (writing the HTML):
|
|
$s.Values
gives us “the raw backing map” for our scratch data.
Because it’s a map, we can assign variables to the key and value of each entry in the map as part of the range
statement:
Generically, this is:
|
|
This capability is actually part of the underlying Go language.
In our specific code, we do this:
|
|
And then because our $tags
variable is an array, we iterate over that array:
|
|
This builds our output.
It’s convenient to use NewScratch
because it comes with those Get
, Set
and Add
functions. But this is all (probably) just syntactic sugar on top of the underlying map used by NewScratch
.
We could perform step 1 using only a map. But it’s a bit more involved, because a map (or rather a dictionary
in Go-speak) is immutable. Instead of updating values in-place (as we did with our scratchpad), here we are always handed a new object which results from some change we make to an existing object. The new object may be referenced by a variable which has the same name as the old object - but it is, nonetheless, a new object. We have not changed (or “mutated”) the original (“immutable”) object. Instead, we have discarded it in favor of the new object.
Why does any of this matter? Because it affects the code we need to write…
Here is that code for our revised step 1:
|
|
We start with an empty map using $tagsMap := dict
.
But what is going on in the line of code below?
|
|
Working from the innermost nested command, outwards:
|
|
The index
function will return null
(when dumped to JSON - which is perhaps Go’s nil
- I don’t actually know for sure) the very first time a key is used. Otherwise it will return the array of tags created so far.
The append
function…
|
|
…will append the tag name to the end of the existing array, and returns the new array. If the array is null
(see above) then a new array will be created containing the tag.
That last point kind of surprised me, but it works. Yes, you can append a value to the end of a null
(or nil
) array in Hugo:
|
|
The first debug.dump
prints null
.
The second one prints [ "xanadu" ]
. Surprise! Other languages I am familiar with would have thrown in the towel at this point (e.g. Java’s null pointer exceptions).
|
|
Back to our one-liner:
|
|
The dict
function…
|
|
…creates a new dictionary (map) containing our key and value-as-an-array.
Finally, the merge
function merges our new map into the existing $tagsMap
. If the key of the new map does not yet exist in $tagsMap
, then the entry (the key and its value) is just added to $tagsMap
.
But if the key does already exist then the new value (the new array) replaces the old value (the old array).
A new dict
is returned at the end of all this - and is assigned back to our $tagsMap
variable, replacing the old map.
(This is the “immutable” nature of Go collections in action - we always return a new object after performing some modification to the existing object - in contrast to NewScratch
, where we are always operating on the same scratch object.)
That completes step 1 of our revised 2-step process.
To complete step 2, instead of iterating over a NewScratch
, we iterate over our $tagsMap
:
|
|
I prefer the readability of the NewScratch
code - although behind the scenes, Hugo is probably just doing something similar to what my dict
code does.
And if you really care about performance, it might be ever so slightly faster to implement all this as a one-pass approach (as mentioned near the start of this post).