11.1 Template Syntax: Actions, Pipelines, and Variables
Alright, let’s get our hands dirty with the syntax. This is where we move from staring at the blueprint to actually swinging the hammer. Go’s template syntax is elegant in its minimalism, but that minimalism can be deceptive. It’s a small set of tools, but you have to learn the precise, slightly opinionated way the designers intended you to use them. Master these fundamentals, and the rest of Hugo falls neatly into place.
The Core Concept: Everything is a Pipeline
Forget what you know about other templating languages for a second. The central, most important idea in Go templates is the pipeline. Think of it literally: data flows from left to right through a series of commands, getting filtered or transformed at each step.
A pipeline is just a chain of one or more “commands,” separated by the pipe character |. The simplest pipeline is a single command, which could be a variable or a function.
{{ .Title }} <!-- A pipeline with one command: the variable .Title -->
{{ .Title | upper }} <!-- A pipeline: .Title flows into the `upper` function -->
{{ .Title | upper | safeHTML }} <!-- A pipeline: .Title -> upper -> safeHTML -->
The output of the previous command becomes the last argument of the next command. This functional, data-flow style is why Go templates are so powerful for chaining transformations. It’s the heart of everything.
The Workhorses: Actions
Actions are the built-in control structures that make your templates dynamic. They’re denoted by the double curly braces {{ }}. Let’s break down the ones you’ll use constantly.
If/Else: This is your primary tool for conditional logic. It’s brutally straightforward.
{{ if .IsHome }}
<h1>Welcome to the Homepage, Sucker!</h1>
{{ else if eq .Type "posts" }}
<h1>A Brilliant Piece of Writing</h1>
{{ else }}
<h1>Some Other Page</h1>
{{ end }}
A crucial “gotcha” here: the if statement completely destroys the pipeline context inside its block. Notice how we just use .IsHome and .Type directly? That’s because inside that block, . is re-scoped to the conditional value. It’s a bit annoying, but you get used to it.
With: Speaking of scoping, meet your best friend, with. It’s like an if statement that also changes the context . for its block. Use it to safely navigate nested data structures.
<!-- If .Params.author exists, then inside this block, . becomes .Params.author -->
{{ with .Params.author }}
<p class="byline">Written by {{ . }}</p>
{{ else }}
<p class="byline">Author Unknown</p>
{{ end }}
This is infinitely safer than doing {{ .Params.author }} directly and hoping it exists. If .Params.author is empty (nil, false, 0, an empty string, array, or map), the else block executes. This is your first line of defense against template rendering errors.
Range: This is your for loop for iterating over slices, arrays, maps, or channels. Just like with, it re-scopes the context . for you.
<ul>
{{ range .Pages }} <!-- .Pages is a slice of pages -->
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li> <!-- Here, . is each individual page -->
{{ end }}
</ul>
<!-- You can also capture the index and/or key -->
{{ range $index, $page := .Pages }}
<p>{{ $index }}: {{ $page.Title }}</p>
{{ end }}
If the thing you’re ranging over is empty, the whole block is skipped. No errors, no fuss. It’s beautifully defensive.
Variables: Your Escape Hatch
The scoping of if, with, and range is powerful, but what if you need access to the original context from inside one of those blocks? This is the most common pitfall for beginners. You don’t have to lose your parent context. You can create variables.
{{ $root := . }} <!-- Save the top-level context for later! Do this at the top of your template. -->
{{ range .Pages }}
<article>
<h2>{{ .Title }}</h2> <!-- . is the current page -->
<p>This site is called {{ $root.Title }}</p> <!-- $root is the original context -->
</article>
{{ end }}
You can also use variables to capture the output of a function or pipeline for later use, saving you from having to call a potentially expensive function twice.
{{ $featuredImage := .Resources.GetMatch "featured*" | images.Fit "600x400" }}
<!-- Now I can use the processed $featuredImage multiple times without reprocessing it -->
<img src="{{ $featuredImage.RelPermalink }}" width="{{ $featuredImage.Width }}">
<meta name="twitter:image" content="{{ $featuredImage.Permalink }}">
The Weird One: Define
{{ define }} isn’t an action you use in your regular layouts; it’s how you create those reusable blocks that you then call with {{ template }} or {{ block }}. Think of it as a named sub-template.
{{ define "head-custom" }}
<!-- This block of code isn't rendered here. It's just defined. -->
<style>
.my-special-class { color: rebeccapurple; }
</style>
{{ end }}
<!-- Later, or in a different template, you can execute it -->
<head>
<title>{{ .Title }}</title>
{{ template "head-custom" . }} <!-- The . here is the context passed to the defined block -->
</head>
Honestly, in Hugo, you’ll use {{ block }} more often than {{ define }} and {{ template }} because of the way it works with the baseof.html pattern, but it’s important to know it exists. The designers were clearly fans of the DRY principle, even if the syntax feels a bit academic at times.
The key to all of this is understanding the context dot . and how it moves. It’s the camera lens through which you view your data. With, range, and if are the commands that change your lens. Variables are how you keep a spare lens in your pocket. Get comfortable with that, and you’ve already lapped most people trying to use Hugo.