OVERHEAD

Simpler, robust logic

Deciphering Hugo

Less is more, if you know more

if is one of the most fundamental functions in any coding or scripting language, often being the easiest concept to understand; “if this, do that”, however, there are many cases where, ironically, using the if function is not the simplest solution. Take the following example:

A Hugo template example, which generates a <meta name="description"> line depending on if .Description or .Site.Params.description is set, with .Description being favoured in case both are set.

{{ if .Description }}
	<meta name="description" content="{{ .Description | markdownify | plainify | safeHTML }}">
	{{ else if .Site.Params.description }}
	<meta name="description" content="{{ .Site.Params.description | markdownify | plainify | safeHTML }}">
{{ end }}

There are a number of problems with this example:

  1. The <meta> line is duplicated for each variable.
  2. The variables themselves are duplicated, by being declared in the if statement and inside the content value.
  3. The markdownify | plainify | safeHTML sanitation functions are duplicated too (see the range of choices post for what this does).

When constructing fallbacks for variables like this, it’s usually better to use a different function: default (see the default function documentation for more information). With this function, we can simplify our previous example:

A variation of the previous example, which includes the default function and no else if statements.

{{ if or .Description .Site.Params.description }}
	<meta name="description" content="{{ default .Site.Params.description .Description | markdownify | plainify | safeHTML }}">
{{ end }}

default .FallbackVar .Var or .Var | default .FallbackVar.

Looking at slices

Working with string variables is relatively simple, but a slice can cause templates to explode in complexity because of functions like range, delimit, and, as we touched on before, duplicate lines within if statements. Here’s an example:

A template example, which generates a <meta name="keywords"> line for either .Params.series, .Params.tags, or .Site.Params.keywords (in that order).

{{ if .Params.series }}
	<meta name="keywords" content="{{ delimit .Params.series ", " }}">
	{{ else if .Params.tags }}
	<meta name="keywords" content="{{ delimit .Params.tags ", " }}">
	{{ else if .Site.Params.keywords }}
	<meta name="keywords" content="{{ delimit .Site.Params.keywords ", " }}">
{{ end }}

For extra complexity, let’s say we also want .Params.series and .Params.tags to be listed in the content value together, with .Site.Params.keywords being the fallback in case both the series and tags variables are unset. Already, this will grow into a iffy nest of nested if statements.

So first, let’s involve the default function:

A variation of the previous example, which uses the default function.

{{ if or .Params.series .Params.tags .Site.Params.keywords }}
	<meta name="keywords" content="{{ delimit (.Params.series | default .Params.tags | default .Site.Params.keywords) ", " }}">
{{ end }}

Instead of trying to handle .Params.series and .Params.tags separately, with each being delimited next to each other to “combine” them inside of the content value, we can actually just combine the slices together with the union function:

A variation of the previous default function example, which combines .Params.series and .Params.tags with the union function.

{{ if or .Params.series .Params.tags .Site.Params.keywords }}
	<meta name="keywords" content="{{ delimit (union .Params.series .Params.tags | default .Site.Params.keywords) ", " }}">
{{ end }}

And finally, since delimiting a slice retains the order its items were specified in, such that ["Tag 1", "Tag 2"] and ["Tag 2", "Tag 1"] are different, we’ll pipe everything through the sort function for a deterministic list:

A variation of the previous union function example, which pipes all the variables through the sort function before being delimited.

{{ if or .Params.series .Params.tags .Site.Params.keywords }}
	<meta name="keywords" content="{{ delimit (union .Params.series .Params.tags | default .Site.Params.keywords | sort) ", " }}">
{{ end }}

Right, I should probably talk about the sort function more, you’ll see why…

Let’s sort this

The sort function is deceptively difficult to use, mainly because its limitation is poorly explained in the sort function documentation (hence why this blog series exists), and requires you to always do one thing: The input must be a slice.

…Well, yeah, of course it does; it should be as simple as doing sort .Params.tags and-NOPE. Do that, and Hugo will slap you with a vague failed to render pages: sequence must be provided error message. Here’s the sneaky detail: .Params.tags is only a slice when set. So, unless .Params.tags is set on every single page, you’ll get this error message.

Thankfully, the fix is really simple: sort (default slice .Var). The only reason this fix wasn’t needed in the previous examples is because .Site.Params.keywords is set globally, and therefore, the sort function always gets a slice (but, if you can’t assume that variable is always set, you’ll need this fix).

Legitimate construction

Lucky URLs

If you’re manually constructing URLs or paths, do not use generic printf or replace functions to insert or remove slashes and other characters, and instead, use the various path and urls functions. These functions can deal with extraneous or missing slash characters in paths, extracting specific sections from URLs as desired, and simplify your template overall while being robust.

To handle slashes in paths:

  • path.Join "/" $taxonomy $term -> /tags/hugo
    • Notice that the leading slash before “tags” won’t be inserted unless “/” or $taxonomy := "/tags" is given to the path.Join function.
    • If you are setting $term by parsing .Params.tags, make sure to pass the names through the urlize function (e.g. path.Join "/" $taxonomy (urlize $term)).
  • path.Clean "/tags/hugo/" -> /tags/hugo
    • This is particularly useful for the .GetPage function, which can’t fetch the paths of pages with a trailing slash (e.g. .GetPage "/tags/hugo/" won’t fetch anything).

To parse URLs:

  • (urls.Parse "/blog/post/#fragment").Path -> /blog/post/
    • This is also useful for the .GetPage function for similar reasons as above.

Lackluster comparisons

Generally speaking, you should never use .Title and similar user-content variables to match pages, such as using an if statement in a range .Pages function to check if the current page is in the list, as the sanitation steps are both error-prone and unnecessary. Instead, try to use more stable and appropriate variables like .Permalink or .RelPermalink, which don’t require sanitation when compared. So instead of:

A template example, which searches all .RegularPages pages and checks whether its .Title variable (with sanitation) is equal to the string “my title”.

{{ range .Regular.Pages }}
	{{ if eq (.Title | markdownify | plainify | lower) "my title" }}
		...
	{{ end }}
{{ end }}

You should instead do:

A variation of the previous example, which instead compares if .Permalink is the same between the found page and the current page (with no sanitation needed).

{{ range .Regular.Pages }}
	{{ if eq .Permalink page.Permalink }}
		...
	{{ end }}
{{ end }}

Last note

And yes, trying to make every heading start with “L” did make writing take much longer than it should of, BUT WAS IT WORTH IT!?

No, not really.