OVERHEAD

Hugo extras

Deciphering Hugo

This post assumes you’ve read all the previous entries in this series. Any variables and functions which haven’t been previously explored, or haven’t been explained fully, will be explained as they appear.

The if function, and template block hyphens

Showcase of the if function, with hyphens inside the template blocks.

{{- if .Var }}outcome1
	{{- elif .Var2 }}outcome2
	{{- else }}outcome3
{{- end }}

Including a hyphen inside a template will cause any whitespace on that side of the template block to be collapsed, so some {{- "" -}} thing would be output as something, which is useful for preventing extra whitespace or newlines from being added because of template blocks themselves.

It’s worth emphasising that {{ example -}} {{- example }} is redundant; you only need one of the hyphens to remove the whitespace between them, and, I recommend being consistent with which hyphen you include. For example, I always use left hyphens, for cases where only one hyphen is needed, so it’s easier to tell when double hyphens are present.

The with function

Showcase of the with function.

{{- with .Var }}outcome1
	{{- else }}outcome2
{{- end }}

Unlike the if function, you can’t use elif inside with statements; only the else function is valid. However, you can nest an if statement under else instead, like this:

Showcase of the with function, with an if statement nested under the else function.

{{- with .Var }}outcome1
	{{- else }}
		{{ if .Var2 }}outcome2
			{{- elif .Var3 }}outcome3
			{{- else }}outcome4
		{{- end }}
{{- end }}

The range function

In the previous posts of this series, I’ve shown off basic uses of the range function to do simple tasks, however, there are times where you’ll need the more advanced functionality it can provide. As an example, let’s say you wanted to print the .Title of each page under the .Pages variable, and you wanted that page’s index in that variable. To accomplish that, you can use the range function’s indexing feature:

Showcase of the range function’s indexing feature.

{{- range $index, $page := .Pages }}
	{{- $index }}: {{ .Title }}, {{ end }}

You must define a variable in the place $page is located, otherwise you’ll get a parse failed range can only initialize variables error.

This will print the index, which starts counting from 0, and the .Title variable for the page at that index.

As an even more specific example, which I use myself, let’s say you wanted to range over the .Params.series and .Params.tags variables together (to save needing separate range statements for each variable), but, you wanted a way to track which taxonomy each term came from so that tags would be given <a rel="tag directory">Tag</a> and series would be given <a rel="category tag directory">Series</a>.

To achieve this, you can use the dict function to define a dictionary of key-value pairs, and then, the index of the range function will be the key’s name. To demonstrate:

Showcase of using the dict and range functions to set strings as the index values.

{{- range $tax, $term := dict "tags" .Params.tags "series" .Params.series }}
	<a rel="{{ if eq $tax "series" }}category {{ end }}tag directory">{{ $term }}</a>
{{- end }}

You could use either $term or . inside the <a> element’s content, however, a deeper nesting of functions may require you to use $term once context rebinding enters the mix.

Additionally, you can use the range function on the $term variable as a way to sneak the sort function in there too:

Showcase of using the dict, range, and sort functions to set strings as index values, while also sorting the list.

{{- range $tax, $term := dict "tags" .Params.tags "series" .Params.series }}
	{{- range sort (default slice $term) }}
		<a rel="{{ if eq $tax "series" }}category {{ end }}tag directory">{{ . }}</a>
	{{- end }}
{{- end }}

Notice the use of the default function to deal with both the .Params.series and .Params.tags variables being unset, read the Simpler, robust logic post for more information.

I do want to stress one limitation with the dict function: It will unconditionally sort its entries by key name such that, in this case, the series tags will be handled before the regular tags since “series” comes before “tags” alphanumerically; the sort function only affects the values under each key. In other words, the dict function ignores the order you specify the key-value pairs in.

There might be a way around this using the group function instead of dict, however, my previous attempts at doing this were unsuccessful. Of course, if you wanted to change the order, you could just change the names of the keys to achieve the order you want, but it’s annoying if you want to print the $tax value as is (which is what I do, but luckily, I actually like the ordering).

The return function, and “custom functions”

As of writing, Hugo doesn’t support custom functions without changing the code of the hugo binary (see the Hugo GitHub repo for progress on that front), but until then, you can achieve something similar using partials. For example, let’s say you wanted a custom function which outputs a string after its been fed through multiple sanitising functions such as markdownify and plainify:

Example of a “sanitise.html” partial using the return function.

{{- return (replace . "&shy;" "" | markdownify | plainify | safeHTML) }}

You can’t execute the return function multiple times in a single partial, but instead, you can use slice or dict to “return” multiple values.

Now, you can use {{ partial "sanitise.html" "Test *str&shy;ing*" }} and receive Test string as the output. But, what if you wanted to pass an argument to this “function” too?

Example of a “sanitise.html” partial using the return function, which can take arguments.

{{- $return := replace .val "&shy;" "" | markdownify | plainify | safeHTML }}
{{- if eq .arg "nosafe" }}
	{{- $return = replace .val "&shy;" "" | markdownify | plainify }}
{{- end }}
{{- return $return }}

With this change, you can specify {{ partial "san.html" (dict "arg" "nosafe" "val" "Test *str&shy;ing*") }} to omit the safeHTML function, but, you need to always use the dict function to specify the value of “val”.

This partial could likely be improved so you could pass the value both with or without the dict function, however, I’ll leave that as an exercise for you.