As we covered in several articles before including this series’ first part, Hugo as a template engine focuses mainly on building template files. As a result, even its most valued partials, while very good at printing stuff, were until this year unable to return typed value.
It all changed with Hugo 0.55.0 when the return
statement was introduced to the partial API! Then all of a sudden partials became powerful reusable functions to be consumed by more conventional template files!
In the second part of our Full Partial Series we’ll first cover the fundamentals of this partials’ feature before getting into some pretty detailed code use cases!
Before return
Before return
, when partials could only print, a lot of people still relied on them to create reusable pieces of code that served other purposes than simple templating. For example, you could turn a relative image url to its S3 counterpart like so:
# layouts/partial/FormatURL.html
{{- $S3Domain := site.Params.s3Domain -}}
{{- printf "%s/%s" $S3Domain . | safeHTMLAttr -}}
# layout/_default/single.html
{{ with .Params.image }}
<img src="{{ partial "FormatURL.html" . }}" />
{{ end }}
The above {{- -}}
made sure nothing else got printed alongside the URL string, no whitespace, no line-break etc.
You could even get further and print jsonified data to be interpreted by the partial call.
# layouts/partial/GetSEOData.html
{{- with . -}}
{{- $title := .Title -}}
{{- $description := .Summary -}}
{{- with .Params.seo.title -}}
{{- $title = . -}}
{{- end -}}
{{- with ..Params.seo.description -}}
{{- $description = . -}}
{{- end -}}
{{- dict "title" $title "description" $description | jsonify -}}
{{- end -}}
# layouts/partial/head.html
{{ $seo := partial "GetSEOData.html" . | transform.Unmarshal }}
{{ with $seo }}
# seo tags...
{{ end }}
But the above trickery was limited to basic parseable types like strings or integer and slices or maps of the later. There was no way to safely return a complex object like a Page or a File and certainly no collection of pages.
Then came Hugo 0.55.0, the return
statement and returning partials!
A few things of note.
Before we dive into the code there is a few do’s and don’ts we should really cover.
🚫 No return inside clauses
{{ if gt .Params.temperature 70 }}
{{ return 😎 }}
{{ end }}
This ☝️ will ignore the value of .Params.temperature
and systematically return 😎. A partial will just return anything that follows a return
statement, regardless of its position in the code.
{{ with .Params.temperature }}
{{ return . }}
{{ end }}
Same with this one ☝️, return
must live at the root and cannot be called from a nested context.
🚫 No multiple return statements
{{ if gt .Params.temperature 70 }}
{{ return 😎 }}
{{ else }}
{{ return ⛸️ }}
{{ end }}
The above is not going to work! You cannot have multiple return statements.
👍 One returning variable
Reviewing the above big no-nos of returning partials one universal rule emerges: One return
at the root.
I find the best way to comply with this is to focus our attention on one single returning variable.
The returning variable is the base of your work on which to add upon and eventually return.
- 🍽️ Init a variable with a default value,
- 🔪 work it,
- 💁♂️ and return it at the end.
{{ $emoji := ⛸️ }}
{{ if gt .Params.temperature 70 }}
{{ $emoji = 😎}}
{{ else if gt .Params.temperature 100 }}
{{ $emoji = 🥵}}
{{ end }}
{{ return $emoji }}
No matter how many lines, whitespaces or linebreaks in your code, the only value produced by your partial is that single emoji which follows the magical word: return
.
Just like, if
, with
, range
and friends, whatever follows return
does not need to be enclosed in parenthesis.
{{ return gt .Params.temperature 50 }}
☝️ Is valid and will return the produced boolean.
🤙 Calling our returning partials.
Just like any other partial!
Emoji: {{ partial "emoji.html" . }}
But real power comes with storing its returned value!
{{ $emoji := partial "emoji.html" . }}
📏 Some conventions!
Or mine anyway…
To make a good distinction between my regular outputting partials and the returning ones, I usually store returning partials’ files in a layouts/partials/func/
directory. This has the benefit of isolating them from any other conventional partials while not adding too many characters to their “calling”.
{{ partial "func/emoji.html" . }}
Also, as Hugo will always assume html
when a partial’s extension is absent from the path argument, I always use .html
for my returning partial files. This way I can safely lose the extension and gain back on those 4 extra chars:
{{ partial "func/emoji" . }}
Finally, I like to capitalize them and add some verb when applicable:
{{ partial "func/GetEmoji" . }}
Voilà, beautiful Hugo:
{{ with partial "func/GetEmoji" . }}
Emoji: {{ . }}
{{ end }}
Coding our returning partials
Alright, theory was interesting but it’s time to crack some knuckle and hit that keyboard! Together we’ll try and answer a very basic need: Efficiently listing taxonomy terms in our Hugo projects.
The above is not always super intuitive. Taxonomy and their terms do hold a special place in a Hugo project, but from the Page context, they’re just another list of strings in your Front Matter.
---
title: A Night at the Metropolitan Museum
tags:
- Art
- New York
- Museum
---
In order to list them as a clickable links, you have to build said links yourself based on those urlized
strings or retrieved their page object using .GetPage
or .Site.Taxonomies
.
It would be nice if this cumbersome work could be processed from within a reusable returning partial and not in our posts’ template files.
As for the way we want to call such partial, it should be as simple as that:
{{ range .Params.tags }}
{{ with partial "func/GPetTagPage" . }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}
🙌 ⌨️ Here we go.
{{/* 1. */}}
{{ $tag := false }}
{{/* 2. */}}
{{ with index site.Taxonomies.tags (urlize .) }}
{{/* 3. */}}
{{ with .Page }}
{{/* 4. */}
{{ $tag = . }}
{{ end }}
{{ end }}
{{/* 5. */}
{{ return $tag }}
- We start with our returning variable and its default value.
site.Taxonomies.tags
returns a collection of all the site’s tags with their .Page
object.
The dot is the partial’s context, our tag’s name, we urlize
it to match its key inside site.Taxonomies.tags
.
3. Pretty sure we’ll find a .Page
, but with
adds extra security and has the benefit of shifting the context.
4. We store the tag’s page in our returning variable.
5. 🎉
👍 Good work!
Now what about other taxonomies like categories
? No way we’re copying/pasting this into a new partial and switch site.Taxonomies.tags
with site.Taxonomies.categories
. 🙅♀️
This is what we want:
{{ range .Params.tags }}
{{ with partial "func/GetTermPage" (dict "taxonomy" "tags" "term" .) }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}
Handling arguments
For now we’ve been dealing with “single argument” returning partials where the context only holds one thing. But here, we need two, the taxonomy and its term. Our context will now need to be a map with those two.
And so we update our partial:
{{ $return := false }}
{{/* 1. */}}
{{ $taxonomy := "tags" }}
{{ with .taxonomy }}
{{ $taxonomy = . }}
{{ end }}
{{/* 2. */}}
{{ with $term := .term }}
{{ with index site.Taxonomies $taxonomy }}
{{ with index . (urlize $term) }}
{{ with .Page }}
{{ $return = . }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* 3. */}}
{{ return $return }}
- Now that I have a
term
argument, naming the returning variable$term
might get confusing. I’ll just call it$return
now so it really stands out as the returning variable.
Without a .term
argument, the returning variable should really remain empty.
Before going any further we use with
to make sure .term
is set and store its value at the init so we can access it regardless of further context shifting.
The following lines present a great example of context shifting by the way!
3. 🎉
Caching!
Now this is great, but I want a function that lists my page’s tags and returns a slice of maps each containing some structured data. Say .URL
and .Name
. This way if I want to switch from .RelPermalink
to .Permalink
in the future, I can do it in my returning partial rather than in every template file where I print those links.
It’ll give us a great opportunity to call a returning partial from within a returning partial and cache its value.
#layout/partials/func/GetTags.html
{{/* 1. */}}
{{ $return := slice }}
{{/* 2. */}}
{{ with .Params.tags }}
{{ range . }}
{{/* 3. */}}
{{ with partialCached "func/GetTerm" (dict "taxonomy" "tags" "term" .) "tags" . }}
{{/* 4. */}}
{{ $tag := dict "URL" .RelPermalink "Name" .Title }}
{{/* 5. */}}
{{ $return := $return | append $tag }}
{{ end }}
{{ end }}
{{ end }}
{{/* 6. */}}
{{ return $return }}
- We want to safely use
range
on the value of our returning partial In order to make sure the returned value is of typeslice
, we make our returning variable defaults to an empty one. range
fails gracefully on empty slices but breaks builds on any other types. So it’s always safer to wrap ourrange
calls with awith
unless you’re certain your value is a slice.- We’re calling our previously coded returning partial, but this time caching it. For variants we use the value of both its arguments.
- We create a map and store it in a variable for better readability. Because our
$tag
is declared inside therange
context, it can not collide with another$tag
from say, the next tag in the list. - We use the
append
function to add our$tag
map to the slice we’ll ultimately return. - 🎉
And now from our template file:
# layouts/_default/single.html
{{/* 1. */}}
{{ range partial "func/GetTags" $ }}
{{/* 2. */}}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
We know for sure the value returned by our homemade returning partial is a slice, empty or not. So it’s safe to use range
here.
2. We can now use the custom keys of our maps.
3. That’s it!
Room for improvement?
Sure! We could:
- Exclude some Tags from the
GetTags
’s returned slice. - Turn
GetTags
intoGetTerms
, making it taxonomy agnostic. - Finding the right variant for our
GetTags
’s returning partial and usepartialCache
. - Build many more returning partials to cater for all our not-so-templating needs!
Conclusion.
After covering some fundamentals, we were able to easily build two returning partials which will bring great maintainability to one aspect of the site.
Shall we need some posts or all our posts to exclude certain tags? That will happen in GetTags
and GetTags
only!
Shall Hugo introduce a more efficient way to handle taxonomy terms in a future release? We’ll adjust our GetTerm
function and that’s it!
By bridging the gap between reusability and typed data serving, Hugo with its returing partials is finally addressing the need for separation of concern between templating and data handling!
Did I already mention it was to me the biggest game-changer of 2020?
If you have some personal experience or questions about returning partials or if you just want to share what you build following based on what we discussed here, just drop a note or code blocks in the comments!