6.8 The Page Object: Accessing Every Field in Templates
Right, let’s talk about the Page object. It’s the single most important variable in your Hugo templates, the key to the kingdom, the master switchboard. When you see .Site or .Title in a template, you’re tapping into this object. It’s Hugo’s way of taking all the disparate data you’ve slung into YAML, TOML, or JSON front matter and squashing it into one beautifully convenient, if occasionally quirky, Go structure for you to play with.
Think of it as a massive, pre-baked burrito. The tortilla is Hugo’s page logic, and the fillings are all your custom front matter fields. Some bites are pure, predictable black beans (.Title), and others might be a surprise chunk of pineapple someone (probably you, three months ago) decided to add ( .Params.custom_field). Our job is to learn how to navigate this delicious, sometimes chaotic, meal.
The Two Realms of Page Data
Not all data in the Page object is created equal. It’s crucial to understand the distinction between Hugo’s built-in variables and your custom fields. Hugo’s designers, in their infinite wisdom, decided to segregate them. This feels a bit pedantic at first, but you’ll come to appreciate the organization.
The first realm is the top-level, built-in stuff. These are things Hugo cares about deeply because they are essential to its core functionality. You access them directly off the Page object.
<h1>{{ .Title }}</h1>
<p>Published on: {{ .Date.Format "January 2, 2006" }}</p>
<p>This page is {{ if .Draft }}a draft{{ else }}published{{ end }}.</p>
<a href="{{ .Permalink }}">Permanent Link</a>
<ul>
{{ range .Pages }} <!-- .Pages is for a section page's children -->
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
The second realm is for all your custom front matter, the stuff you define. Hugo, politely, doesn’t want to assume anything about your favorite_animal or recipe_rating field. So it dumps all of it into a special bucket called .Params. This is a catch-all map (a key-value store) for your creativity, or your client’s questionable demands.
<p>My favorite animal is the {{ .Params.favorite_animal }}.</p>
<!-- Want a subtitle? Just make a 'subtitle' field in your front matter! -->
<h2>{{ .Params.subtitle }}</h2>
<!-- How about a list of ingredients? -->
<ul>
{{ range .Params.ingredients }}
<li>{{ . }}</li>
{{ end }}
</ul>
The rule is simple: if Hugo provides it (like .Title, .Date), it’s top-level. If you provided it in front matter, it’s in .Params.
Accessing Nested and Dictionary Data
This is where the magic, and occasional frustration, happens. Your front matter isn’t limited to simple strings and arrays. You can build deeply nested structures. Accessing them is straightforward, but the syntax is a common trip-up for newcomers.
Let’s say you have this in your article’s front matter:
author:
name: "Anya Forger"
bio: "A skilled spy. And a good girl."
social:
twitter: "anyas_personal"
github: "anyas_work"
To access this in your template, you chain the keys together using dots, starting from .Params. It’s like giving directions to the data.
<article>
<h1>{{ .Title }}</h1>
<p class="byline">By {{ .Params.author.name }}</p>
<p>{{ .Params.author.bio }}</p>
<p>Find me on <a href="https://twitter.com/{{ .Params.author.social.twitter }}">Twitter</a>.</p>
</article>
The most common pitfall here is trying to access a nested field that doesn’t exist. If there’s no author.social defined for a particular page, Hugo won’t throw a fiery error; it will just render nothing. This is usually what you want, but it’s why you must be meticulous about your front matter consistency across content types.
The Perils of Missing Data and How to Defuse Them
So you’ve built a beautiful template that expects every page to have a hero_image. Then you create a new page and forget to add it. What happens? Hugo, ever the stoic butler, simply evaluates .Params.hero_image to nil and moves on, leaving you with a broken image tag.
This is where the if statement becomes your best friend. Never assume a custom field exists. Always check.
<!-- The risky way (don't do this) -->
<img src="{{ .Params.hero_image }}" alt="Hero image">
<!-- The robust way (do this) -->
{{ with .Params.hero_image }}
<img src="{{ . }}" alt="Hero image">
{{ else }}
<img src="/images/default-hero.jpg" alt="Default hero image">
{{ end }}
The with block is the idiomatic Hugo way to handle this. It says “if this value exists and is not empty, then render what’s inside this block, using that value as the context.” It’s clean, readable, and prevents a whole class of template errors.
For more complex logic, you might need if and isset. isset is a function that checks if a specific key exists in a map, which is perfect for nested .Params.
{{ if isset .Params "author" }}
{{ if isset .Params.author "social" }}
{{ if isset .Params.author.social "twitter" }}
<a href="https://twitter.com/{{ .Params.author.social.twitter }}">Twitter</a>
{{ end }}
{{ end }}
{{ end }}
Yes, it’s a bit verbose. It’s the price of admission for robustness. Alternatively, you could use the slightly more concise with chain, though it can get awkward:
{{ with .Params.author }}
{{ with .social }}
{{ with .twitter }}
<a href="https://twitter.com/{{ . }}">Twitter</a>
{{ end }}
{{ end }}
{{ end }}
The best practice? Structure your content types thoughtfully to minimize these checks. If a field is truly required for a layout, make it a required field in your content creation process, because Hugo sure won’t.