Hugo and Data: Advance manipulation with Slices and Maps
Building or manipulating data to one’s need from a raw source — like an API endpoint or a Markdown file — is not often discussed in the Hugo community and may seem daunting for many users.
Yet masterting data with Hugo can prove critical especially when building pages from a remote source!
How can Hugo and Go Template turn your massive wall of JSON or an editor friendly Front Matter into just what your HTML needs? In this article we’ll cover advanced data manipulation in Hugo with Slices and Maps types. We’ll learn how to filter their content and shape up their structure while reviewing some critical functions and concepts of Hugo.
Prerequisite
You’ll need an intermediate understanding of Go Template in Hugo. If you’re unsure, it might be a good idea — it’s always a good idea — to review the basics from this earlier article: Hugo and Data: The Basics
Having a Hugo running on your local machine is a plus, as you’ll be able to experiment with the code examples! But if not, just read along and imagine a cocktail in your hand!
Slices
Slices are arrays, they can be made of strings, maps, Pages etc…
Creating a slice
We create a slice using the slice
function:
{{ $gents := slice "John" "Paul" }}
{{ range $gents }}
I love {{ . }}
{{ end }}
The above will print:
I love John I love Paul
Adding to a slice, or append.
We use the append
function. It takes two parameters:
- First parameter, is what we add to the slice,
- second parameter is the slice in question.
{{ $gents = append "Ringo" $gents }}
In plain english: Append Ringo to the Gents
Note that if your first argument is a another slice, it’ll add all its entries!
{{ $gents = append (slice "Ringo" "George") $gents }}
Append Ringo and George to the Gents
Now we’ll have:
I love John I love Paul I love Ringo I love Ringo I love George
Oops, Ringo’s been added twice, once as a lone string
argument and once as part of that [Ringo George] slice
; no sweat we can use uniq
to ensure there’s no duplicate:
{{ $gents = uniq $gents }}
Prepend?
Sure, there’s no prepend
function, but append
will do. Instead of adding Ringo (1st argument) to the slice [John, Paul] (2nd argument) we want to add the slice [John, Paul] to the slice [Ringo]… We just revert the argument order and make sure Ringo is wrapped in a slice:
{{ $gents = append $gents (slice "Ringo") }}
Or in plain english: Append the Gents to Ringo
Go Template has that pipe thing that allows to chain functions. The offsetting thing is that the left part will be passed as the last argument of the chained function. And when you append to a slice, the order matter.
{{ $gents = $gents | append "Ringo" }}
Actually means: Append “Ringo” to the Gents or The Gents are taking Ringo in… 🤷
Index
If you want to retrieve the index from a range on a slice, you can actually store it alongside the value at cursor like this:
{{ range $index, $gent := $gents }}
{{ $gent }} is at index {{ $index }}
{{ end }}
Will print:
John is at index 0
Paul is at index 1
Ringo is at index 2
George is at index 3
Yep, pretty much like in any programming language, the index starts at 0.
Even though our value is now stored in a $value
variable, it remains available in the .
.
{{ range $index, $value := $gents }}
{{ . }} is at index {{ $index }}
{{ end }}
First, Last
What if we only want our first 2 gents? Or the last 3? We can use first
or last
. Both functions take two parameters, an integer and a slice.
{{ $first_two := first 2 $gents }}
{{ $last_three := last 3 $gents }}
This reads nicely don’t you think? Now both functions always return a slice, even if it contains a lone gent like {{ $first := first 1 $gent }}
The other index
.
First and last is good, but what if we want just the 3rd one. Well we could do:
{{ $third := first 3 $gents | last 1 }}
We could but…should we? We’d rather use index
. This function takes two parameters, a slice (or a map, more on that later) and an integer for the index. So to find the third gent:
{{ $third := index $gents 2 }}
Two because, the index starts at zero!
index . 0
to single out a lone element wrapped in a slice.Maps
Maps are associative arrays, meaning you’ve got key and value pairs.
Creating a Map
To create a map we use the dict
function. It takes an unlimited sets of even parameters: odds are keys, evens are values.
{{ $gent := dict "firstame" "John" "lastname" "Lennon" }}
Since Hugo 0.81.0 we can break lines within “curlies”. This enables more readable declarations.
{{ $gent := dict
"firstname" "John"
"lastname" "Lennon"
}}
Firsname: {{ $gent.firstname }}
Lastname: {{ $gent.lastname }}
Beautiful!
The above will print:
Firstname: John
Lastname: Lennon
Add to a map
The simplest way is to use the merge function:
The merge function takes two parameters, two maps which will be merged into one together. By creating a new map with its own pair of key values and merging it on top of the existing one, we add to a map or edit the value an existing pair:
{{ $gent = merge $gent (dict "birth" "1940") }}
Browsing the map
We range on the map the same way we do a slice. Index is also available, although this time it holds our key.
{{ range $key, $value := $gent }}
{{ $key }}: {{ $value }}
{{ end }}
Will print:
birth: 1940
firstame: John
lastname: Lennon
You’ll notice that the order does not follow the one we used. That’s because Hugo (Go) systematically re-order pairs by their keys. Slice on the other hand will always keep their defined order.
Ok! Now that gent played several instruments with his band, and we’ll break lines to make it a bit more elegant.
{{ $gent = merge $gent (dict
"instruments" (slice
"Piano"
"Guitar"
"Vocals"
)
) }}
Now our range
will print:
birth: 1940
firstame: John
instruments: [Piano Guitar Vocals]
lastname: Lennon
The instruments value does not look so good though! We should test if our value is a slice, and have it behave appropriately. We’ll use delimit
to join the slice’s value into a string with a comma as delimiter.
As for testing, for now Hugo can only test for two types with the following self explanatory functions: reflect.IsSlice
and reflect.IsMap
.
Here we go:
{{ range $key, $value := $gent }}
<div>
{{ $key }}:
{{ if reflect.IsSlice $value }}
{{ delimit $value ", " }}
{{ else }}
{{ $value }}
{{ end }}
</div>
{{ end }}
Will print:
birth: 1940
firstame: John
instruments: Piano, Guitar, Vocals
lastname: Lennon
The other index
.
We covered index
as a function for slices, but it also works on maps. Only this time the second parameter is a string holding a key from the map.
{{ $firstname := index $gent "firstname" }}
This function will prove very valuable on maps, when the key in question is stored in a variable:
{{ $basis := "lastname" }}
{{ if eq $relation "friend" }}
{{ $basis = "firstname" }}
{{ end }}
Good day {{ index $gent $basis }}!
Or a range:
{{ range slice "firstname" "lastname" }}
{{ . }}: {{ index $gent . }}
{{ end }}
Slices of Maps
Now let’s mix slices and maps!
We’ll create a slice of maps. We already have one gent so we “instantiate” our slice with him:
{{ $gents := slice $gent }}
Now we can add a new gent in a readable way with linebreaks and pipes:
{{ $gents = $gents | append (dict
"firstname" "Paul"
"lastname" "McCartney"
"birth" "1942"
"instruments" (slice
"Bass Guitar"
"Guitar"
"Vocals"
)
) }}
And in order to add the last gents, we can append a slice of the two:
{{ $gents = $gents | append (slice
(dict
"firstname" "Ringo"
"lastname" "Starr"
"birth" "1940"
"instruments" (slice
"Drums"
"Vocals"
)
)
(dict
"firstname" "George"
"lastname" "Harrison"
"birth" "1943"
"instruments" (slice
"Guitar"
"Sitar"
"Vocals"
)
)
) }}
Browsing
And now to browse our gents:
{{ range $gents }}
<p>
{{ .firstname }} {{ .lastname }} was born on {{ .birth }}, he played {{ delimit .instruments ", " " and " }}
</p>
{{ end }}
Will print:
John Lennon was born on 1940, he played Piano, Guitar and Vocals
McCartney was born on 1942, he played Bass Guitar, Guitar and Vocals
Ringo Starr was born on 1940, he played Drums and Vocals
George Harrison was born on 1943, he played Guitar, Sitar and Vocals
Filtering
People are using where
clause all the time to filter out pages and if you’re not familiar you should give it glance in the
doc, but you can use it on any kind of collections, slices as well.
Let’s find all gents born in 1940
As you know, where
returns a slice empty or not, so it’s safe to use range/else on it.
{{ range where $gents ".birth" "1940" }}
<p>{{ .firstname }}, {{ .birth }}</p>
{{ else }}
No gents 🤷!
{{ end }}
Now all the gents not born in 1940
{{ $gents := where $gents ".birth" "!=" "1940" }}
Or born in or after 1942
{{ $gents := where $gents ".birth" ">=" "1942" }}
As you’ll have noticed by now where
takes a set of parameters.
- The first,
$gents
is the collection. - The second is the key we’re evaluating in the collection’s entries
".birth"
. (dot is optional, but I find it helps instantly identify the “key” argument). - The third is the operator in use.
- The fourth is the value we’re trying to match.
If you omit number two, the operator defaults to "=="
I woudn’t go as far as writing a javascript comparison but… Ok just one!
gents = gents.map((gent) => gent.birth >= 1942);
1942
(int) as matching value.Back to playing: Born in 1942 OR 1940?
{{ $gents := where $gents ".birth" "in" (slice "1940" "1942") }}
Here we can use "in"
to find gents whose birth year is included in [“1940”, “1942”]
Now we want all gents playing the Guitar!
It’s different that with "in"
as we want to find gents whose instruments’ list includes “Guitar”.
We’ll use the "intersect"
operator. It compares a slice from the entries with a given slice, and only returns the entries where both slices “intersect”.
[Guitar, Piano] and [Bass, Guitar] intersects! They have Guitar in common!
[Guitar, Piano] and [Drums, Vocals] do not intersect! Nothing in common!
[Guitar, Vocals] and [Drums, Vocals] do intersect, but with Vocals, not Guitar.
Now we only need one intersection, Guitar, so:
{{ $gents := where $gents ".instruments" "intersect" (slice "Guitar") }}
And if we wanted to find the gents who played Guitar and Vocals — two intersections — we’d:
{{ $gents := where $gents ".instruments" "intersect" (slice "Guitar" "Vocals") }}
Sorting
Some of you might be familiar with the way pages are sorted with Hugo but this is only for pages, not any kind of colletions, like our gents.
For those there is a sort
function.
Let’s sort our gents by age using their birth
year:
{{ $gents := sort $gents ".birth" }}
sort
takes a first parameter, the key we’ll sort the collection by, and an optional second for the direction. It defaults to ascending.
To reverse the order and have them younger to older, we add a third parameter, desc
for — you guessed it — descending.
{{ $gents := sort $gents ".birth" "desc" }}
Again the .
in the key parameter is optional.
To reality and beyond!
Ok, that is all well and good, but let’s be honest, hardcoding your data this way can be useful for defining defaults or re-usable bases but not much more.
Usually your data comes from a source you lack control of like an API, or a Data file or most usually a user managed content file.
In the follow up article Hugo and Data: Advanced Transformations we’ll cover how you can take data from a limited source (basic Front Matters, API endpoints) and transform it to data which better suit your project’s needs. We’ll use a lot of stuff covered above, but also more advanced ones so you can code safely with readablity, scalablity and build time in mind!
Comments
💡 Any questions or feedbacks? Start a conversation!- Hugo and Data: The Basics
- Redirects Hugo Module with Netlify
- imgix Hugo Module
- Hugo Modules: everything you need to know!
- Toward using a Headless CMS with Hugo: Building Pages from an API
- Toward using a Headless CMS with Hugo: Building Pages from Data
- What is a Hyperlocal Website?
- Even Better Performance for your Website with Jamstack