DEV Community

Cover image for Advanced Hugo Template Tips and Tricks
Brian Rinaldi for Stackbit

Posted on • Updated on • Originally published at stackbit.com

Advanced Hugo Template Tips and Tricks

Hugo is one of the most popular static site generators. It is a Go-based tool originally created by Steve Francia (now the Go product lead at Google) around 2013 - so it's been around for a while by JAMstack standards.

Hugo uses Go's text and HTML templates that underly a lot of the core features you'll use in every template, but it adds a long list of functions that help you accomplish some pretty complex templating tasks. In this article, I'm going to discuss some of the things you may encounter or need as you develop more complex Hugo templates. This is by no means a comprehensive list, but they are things that I've personally needed (or needed help with) when developing with Hugo.

Scoping

One of the things you will encounter regularly in Hugo once you get beyond the basics are issues around the current scope (sometimes also called context). If you've done any kind of Hugo templating, you'll be used to typing the leading . on a value or even the . by itself but never thought much about it.

The initial template scope is a Page. This is why you can generally access any page variables without any additional scope. For example, if you want to output the title of a page, you simply put {{ .Title }}.

The most common time you'll encounter scope being changed is when looping over values with range. For example, if you are looping over pages on a list template.

{{ range .Pages }}
    <li>
        <a href="{{.Permalink}}">{{.Title}}</a>
    </li>
{{ end }}
Enter fullscreen mode Exit fullscreen mode

In the above example, .Permalink and .Title refer to the current iteration of .Pages rather than the current page. This can get complex when you have multiple nested loops, but generally it makes sense. However, sometimes it is useful to change the base scope, while other times you are likely to encounter weird issues that are often related to scope. Let's look at these.

Régis Philibert has a great deep dive into this topic that is worth checking out: Hugo, the scope, the context and the dot.

with

First, let's look at changing the current scope using with. There are times when your code will be much easier to write and to understand if you change the scope. For example, let's imagine you have social accounts configured in your .Site.Params. Rather than write out the full path like .Site.Params.Twitter for each, you can use with to set the scope.

{{ with .Site.Params }}
<div class="social">
    <a href="https://twitter.com/{{.Twitter}}"><i class="fa fa-twitter"></i></a>
    <a href="https://www.instagram.com/{{.Instagram}}"><i class="fa fa-instagram"></i></a>
    <a href="https://www.youtube.com/channel/{{.Youtube}}"><i class="fa fa-youtube"></i></a>
</div>
{{ end }}
Enter fullscreen mode Exit fullscreen mode

Because of the use of with above, every variable within the block is scoped to .Site.Params.

Using with can also save you from having to write if statements to see if a parameter exists because the block gets skipped if the variable does not exist. In the below example, if .Site.Params.Twitter does not exist, the entire block is skipped (note that we then reference the variable as only the .).

{{ with .Site.Params.Twitter }}
<a href="https://twitter.com/{{.}}"><i class="fa fa-twitter"></i></a>
{{ end }}
Enter fullscreen mode Exit fullscreen mode

In this case, the block functions similarly to if it were surrounded by {{ if isset .Site.Params "Twitter" }}.

I have found with especially useful in combination with GetPage to scope output within a block to the loaded page content. We'll be discussing GetPage in just a bit.

Scratch

I have to admit that for the longest time I never understood the need for .Scratch, but as your templates get more complex, you'll occasionally run into hard to figure out scope issues. It's even a little tough to describe scenarios where it is useful but, most often, when I received seemingly unexplainable errors related to undefined variables (ones that I knew existed), it was usually because I was encountering a scoping issue where .Scratch is useful.

Most of these issues involve when you move into a different scope within a block, but then need to access some of the data outside that block (i.e. when in a different scope). Let's take the following code where, within a with block I need to set a variable. I should note that this example is purposefully contrived to create the problem in a way to make it clear the type of problems .Scratch solves.

{{ with .Site.Params }}
    {{ $greeting := "hello" }}
{{ end }}

{{ $greeting }}
Enter fullscreen mode Exit fullscreen mode

This will result in an error undefined variable "$greeting" even though we can see that it is being set. However, we can use .Scratch to set and get the variable, bypassing the scoping issues (note that I am using the $ to get around the fact that the block is scoped to .Site.Params and thus does not have .Scratch in its scope).

{{ with .Site.Params }}
    {{ $.Scratch.Set "greeting" "Hello" }}
{{ end }}

{{ .Scratch.Get "greeting" }}
Enter fullscreen mode Exit fullscreen mode

As I said, this is an intentionally contrived example and it is worth noting that it can actually be solved by first defining the variable outside the with block so that it is declared within the same scope.

{{ $greeting := "" }}
{{ with .Site.Params }}
    {{ $greeting = "Hello" }}
{{ end }}

{{ $greeting }}
Enter fullscreen mode Exit fullscreen mode

Still, as your templates get more complex you may into these sorts of issues in various places and it's good to be aware that .Scratch might be useful.

GetPage

Sometimes you need to load the contents of another page on a template. For example, a blog post template may require author information that is contained in a page in the /authors directory. I could load my author information that exists at /authors/brian-rinaldi.md as follows:

{{ with .GetPage "/authors/brian-rinaldi" }}
    {{.Title}}
{{end}}
Enter fullscreen mode Exit fullscreen mode

Many times you won't be loading a hardcoded file path. For example, if you are managing relationships between content items, you might have author as a frontmatter attribute on a post. For example, I may have something like:

author: brian-rinaldi
Enter fullscreen mode Exit fullscreen mode

In this case, I want to assemble the path that GetPage will load dynamically. If we had the full path, we could just load it as follows:

{{ with .GetPage .Params.author }}
    {{.Title}}
{{end}}
Enter fullscreen mode Exit fullscreen mode

What if we need to dynamically generate the URL? In this case, I've found that using the print function helps when assembling the string.

{{ $author := print "/authors/" .Params.author }}
{{ with .GetPage $author }}
    {{.Title}}
{{end}}
Enter fullscreen mode Exit fullscreen mode

Complex Querying

If you've done any templating in Hugo, you've probably used the where function to filter an array of results. For example, if I wanted only pages in the blog section, I might do something like this:

{{ range where .Site.Pages "Section" "blog" }}
Enter fullscreen mode Exit fullscreen mode

But one of the amazing things about the where statement is the ability to use it to filter an array you've already filtered and even combine it with other filters to do some pretty complex things. For example, in the following code, I am pulling pages in an "events" section, ordering them by descending date, then getting only those whose frontmatter indicate they are featured. I am skipping the first two results using the after function and then randomizing the order using the shuffle function (of course, the random order only happens at build time).

{{ $remaining := shuffle (after 2 (where (where .Site.Pages.ByDate.Reverse "Section" "events") ".Params.featured" true)) }}
Enter fullscreen mode Exit fullscreen mode

Let's look at the code written another way that achieves the same result but makes it clearer what is going on:

{{ $remaining := where .Site.Pages.ByDate.Reverse "Section" "events" }}
{{ $remaining = where $remaining ".Params.featured" true }}
{{ $remaining = after 2 $remaining }}
{{ $remaining = shuffle $remaining }}
Enter fullscreen mode Exit fullscreen mode

By layering where queries and combining them with other filtering and ordering functions, you can achieve very complex results.

Intersect

One way to perform a complex query of results is using the intersect function, which will find results that overlap. You can use intersect as a function or you can use it within a query. For example, let's say I have pages with frontmatter that indicates which characters it discusses like so:

titans:
  - Robin
  - Starfire
Enter fullscreen mode Exit fullscreen mode

What if I want to get any pages that have one of either "Starfire" or "Raven"?

{{ $titans := where .Site.RegularPages ".Params.titans" "intersect" (slice "Starfire" "Raven")  }}
Enter fullscreen mode Exit fullscreen mode

In this case, the results do not exclude pages that have other characters listed so long as they have at least one matching result (i.e. either "Starfire" or "Raven"). For purposes of example, I've hardcoded the values I want to look for, but in most cases you'd be basing this on a variable. For instance, perhaps I want to show related pages to the current page that discuss the same characters (while excluding the current page from the result):

{{ if isset .Params "titans" }}
    {{ $titans := where (where .Site.RegularPages ".Params.titans" "intersect" .Params.Titans) ".Permalink" "ne" .Permalink }}
    <ul>
    {{ range $titans }}
        <li><a href="{{.Permalink}}">{{.Title }}</a></li>
    {{ end }}
    </ul>
{{ end }}
Enter fullscreen mode Exit fullscreen mode

Index

The index function is most commonly useful when you need to get a specific object in an array of data. For example, let's take the following data set:

---
Robin:
  real_name: Dick Grayson
  species: human
Raven:
  real_name: Rachel Roth
  species: half-Azarathian/Half-Demon
Starfire:
  real_name: Koriand'r
  species: Tamaranean
Beast Boy:
  real_name: Garfield Mark Logan
  species: Metahuman
Cyborg:
  real_name: Victor Stone
  species: Cyborg
Enter fullscreen mode Exit fullscreen mode

I could access the object for Raven via .Site.Data.Titans.Raven, but what if I needed to get that dynamically via a variable or frontmatter page parameter? In this case, index is very helpful:

{{ $character := index .Site.Data.Titans .Params.Character }}
Enter fullscreen mode Exit fullscreen mode

However, index can also be useful in cases where you need to pull a specific index from a data structure. For example, in the following case I need the object in the first record of a query result:

{{ $events := where .Site.RegularPages ".Params.sessions" "intersect" $eventArr }}
{{ $event := index $events 0 }}
Enter fullscreen mode Exit fullscreen mode

Where to Go From Here

Hopefully you find these tips helpful. Obviously, there's a lot that I didn't cover. The first place to check for help is the Hugo docs, which are very well written and comprehensive - always including a relevant code sample. I definitely recommend following Régis Philibert's blog as he covers a lot of beginner and advanced Hugo topics (and thanks to him for his review of this post).

Top comments (2)

Collapse
 
carlmjohnson profile image
Carl Johnson

.Scratch was necessary before Go allowed rebinding template variables. Now, it's not strictly necessary, but Hugo includes it for backwards compatibility, plus it has some nicer APIs (e.g. for building dictionaries with .SetInMap).

Collapse
 
remotesynth profile image
Brian Rinaldi

Thanks for the comment. Yes, I had this discussion with someone who reviewed my article and I think that in most cases it can be avoided but 1) sometimes its just easier to use it; and 2) I have run into cases where I can't seem to remove it (or if I could, it goes back to 1). My use case is simple for example purposes, but I have run into fringe cases where I tried refactoring Scratch out and simply could not fix the scoping any other way. Here's a snippet of the example:

{{ if ne .Params.sessions nil }}
    {{ range $session := .Params.sessions }}

        {{ with site.GetPage (print "/sessions/" $session) }}
            {{ $.Scratch.Set "speakers" .Params.speakers }}
            <h2>{{ .Title }}</h2>
            {{ .Content }}
        {{ end }}
        {{ $speakers := $.Scratch.Get "speakers" }}
        {{ range $speaker := $speakers }}
            ...

In this case, I seem to run into issues with $speakers regardless of where I initially set it. There may be a way to avoid Scratch but, honestly, is it worth the pain of figuring out where the scoping issue is.