11.5 Iteration: range and Its Variables
Right, let’s talk about iteration. This is where Go templates stop being a fancy variable replacer and start feeling like a real programming language. The workhorse here is the range action, and it’s your best friend for looping through all your content, tags, or any slice of data Hugo hands you.
The basic syntax is simple enough. You’ve got your collection, you range over it, and you do something with each item.
<ul>
{{ range .Pages }}
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
See that dot inside the block? That’s the first gotcha. When you’re inside a range block, the context—the dot—changes. It’s no longer the grand, all-knowing page context (.); it becomes the individual element you’re currently looping over. This is incredibly powerful because it lets you access .Title, .Permalink, etc., directly. But it’s also the number one cause of head-scratching errors. You suddenly can’t access that site-wide variable you were using a second ago because you’re now inside the loop, staring at a single page. To get back to the parent context, you have to use $.Site.Title—the $ is your anchor to the top-level scope. Remember that. It’ll save you.
The Dot is a Shape-Shifter
I can’t stress this enough. The dot changes, and you need to know what it’s changed into. When you range over .Pages, the dot becomes a page.Page object. When you range over .Site.Tags, the dot becomes a Taxonomy object, which has a .Page (the term page) and a .Pages (the list of pages with that tag). This is why blindly trying .Title inside a range over a taxonomy might give you nothing—you’re probably on the wrong object. You have to be intentional. Are you trying to get the tag’s name? That’s .Term. The title of the tag’s page? That’s .Page.Title. It’s a bit convoluted, and yes, I agree the designers could have made this more consistent. We play the hand we’re dealt.
Unpacking Key-Value Pairs with $
Sometimes you don’t just get a value; you get a key and a value. Hugo’s taxonomy maps (.Site.Tags, .Site.Categories) and the dict function you might create yourself work this way. The range action lets you capture both.
{{ range $key, $value := .Site.Tags }}
<h3>{{ $key }} ({{ len $value }} posts)</h3>
<ul>
{{ range $value }}
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
{{ end }}
Here, $key is the name of the tag (e.g., “python”), and $value is the slice of pages tagged with it. Why $key and $value? You could call them $tagName and $posts or $burrito and $ingredients—the names are up to you. The important part is the assignment operator :=. This syntax captures the two parts of the key-value pair into variables you control. This is your escape hatch. Now you’ve got the key in $key and the current scope (the value) is still the dot, but you’ve also got it stored in $value for clarity. This is immensely useful for avoiding scope confusion.
The Often-Ignored Loop Variables
Here’s a party trick Hugo inherits from Go that most people forget about: the magic loop variables. Inside a range block, you have access to a set of variables that tell you where you are in the loop:
$index: The 0-based position in the slice you’re looping over. (In a key-value range, this is the key).$indexand$keyare the same thing in a key-value range, which is… a choice. I usually just use the$keyI explicitly defined to avoid confusion.$counter: The 1-based position ($index + 1). Useful for human-readable lists.$first: A boolean,trueif this is the first iteration.$last: A boolean,trueif this is the last iteration.prevandnext: These are weird. They aren’t booleans; they give you the previous or next item in the collection.{{ with .next }}, then you can access its properties.
These are fantastic for adding logic without cluttering your code with counter variables.
{{ range $index, $page := .Pages }}
<article>
<h2>{{ $counter }}. {{ $page.Title }}</h2>
{{ if $first }}<p>This is the lead article!</p>{{ end }}
{{ if not $last }}<hr>{{ end }} // Add a divider after all but the last item
</article>
{{ end }}
Ranging Over Nothing (And Why It Won’t Break)
A beautiful feature of range is its politeness. If you try to range over a nil or empty collection—say, .Pages on a page that has no children, or a tags list that’s empty—it simply does nothing. No error, no output, just graceful silence. This is by design and means you rarely need to wrap your range statements in if blocks just to check if there’s anything to loop over. You can just range. If there’s nothing there, the block is skipped. It’s one less thing to worry about, and I’m all for that.