Handle Last Element Differently When Looping In Hugo
When building smigle, the Hugo theme for this site, I was looping over an array and outputting both the current element and some delimiter text, but I couldn’t figure out how to handle the last element differently from the rest so as to avoid the unwanted trailing delimiter. In this post, I cover the solutions I found for this delimiter issue. Hopefully, this short read saves you from the misery I went through while hunting for advice on Stack Overflow and Discourse.
The Task
The task I was trying to accomplish with the range
builtin function
seemed simple to me. I wanted to build a navbar by looping over an array
of maps, each containing two keys: name and url. One important
requirement of the navbar was that the links needed to be separated by
some delimiter text, a ‘|’ in my case. The tricky part of the
requirement was eliminating the unwanted trailing delimiter.
Having explained the task, let’s dive into the code. In the following code snippet, I create an array of foods and range over the array while creating links within a paragraph. In reality, your array will probably come from your config file, but I wanted this example to be self-contained.
{{/* Snippet-1 */}}
{{ $array := slice
(dict "name" "apple" "url" "https://en.wikipedia.org/wiki/Apple")
(dict "name" "banana" "url" "https://en.wikipedia.org/wiki/Banana")
(dict "name" "carrot" "url" "https://en.wikipedia.org/wiki/Carrot")
}}
{{/* Build delimited links (has trailing delimiter) */}}
<p>
{{ range $array }}
<a href="{{ .url }}">{{ .name }}</a> |
{{ end }}
</p>
The code above produces the unwanted trailing delimiter. The following code shows two options that both fix the trailing delimiter issue.
Two solutions
{{/* Snippet-2 */}}
{{/* Option-1 - pull out array index */}}
<p>
{{ range $index, $value := $array }}
{{ if $index }} | {{ end }}
<a href="{{ .url }}">{{ .name }}</a>
{{ end }}
</p>
{{/* Option-2 - limit the array using first and after */}}
<p>
{{ range first 1 $array }}
<a href="{{ .url }}">{{ .name }}</a>
{{ end }}
{{ range after 1 $array }}
| <a href="{{ .url }}">{{ .name }}</a>
{{ end }}
</p>
The first option looks more DRY, but it could be less performant
since it evaluates a condition during each loop iteration. It works by
using the range
syntax that pulls out the index of the array and then
performing a clever condition check on the index. The condition is met
if the index is truthy, which is the case for all indicies besides the
first one, since 0 is falsey. When that condition isn’t met, only the
link is outputed, and no delimiter. When the condition is met, the link
is prefixed with the delimiter. I came across this option in this
discourse post by the Hugo author
@bep.
The second option makes use of the handy first
and after
builtin
functions which respectively, limit elements of an array to the first n
elements and advance the iterator’s position by n. Those two functions
are used to separate how the first and non-first elements are processed.
I found this option while digging through this Hugo
theme by @sumnerevans.
One tempting non-solution
Before I wrap up this post, I want to point out the delimit
builtin
function, which comes up whenever you do a web search for “hugo range
last delimiter.” Although sounding perfectly relevant, this fuction is
not the right tool for the job. This is because delimit
accepts an
array and a delimiter string and outputs one string with all the array
elements spaced out by the delimiter. Unfortunately, that doesn’t give
the flexibility of range
, which lets you output arbitrary HTML.
{{/* Snippet-3 */}}
{{ $array := slice "apple" "banana" "carrot" }}
<p>
{{ delimit $array " | " }}
</p>
{{/*
Outputs:
<p>apple | banana | carrot</p>
*/}}
The above code demonstrates what delimit
function is capable of. As
you can see, the function doesn’t allow for outputing links using the
the array of name/url maps from before.
You’ve seen two ways of solving the “last delimiter issue” and one tempting yet unhelpful function. Hopefully, this short lesson helps advance your Hugo skills and saves you time hunting through Q&A sites.
Thanks for reading!