Better Relationships in Hugo
with Hugo's Related Content
I recently set my mind on improving the way I dealt with relationships in my projects by using Hugo’s very own Related Content1.
By doing so I slashed the build time by a whopping … 🥁 … 70%!
This article uncovers how easy it is to implement Hugo’s new Related Content feature on an existing project and how it may revolutionize the way we manage relationships in Hugo!
It’s time we get better at handling relationships 👫
The Project
I created this french open source website about Les Rougon-Macquart by Emile Zola long before my coding days.
With a thousand entries sharing a healthy relationship, this is the perfect project to tryout our new implementation:
- Every 1300 or so character belongs to a few novels and lists them on their page.
- Every 20 novel holds a lot of characters and lists them on their page.
Relationship status before Related Content: it’s complicated.
There was no clear way to connect pages together and form consistent and efficient relathionships. Taxonomies came to mind often, but when you needed to connect Regular Pages together, they could not do.
After eliminating Taxonomies, if you were dealing with a One-to-many relationship, you may have used sections
with caution.
But when implementing the most common Many-to-many relationship, I find the most sensible solution was to establish the relationship via a Front Matter Param
inside the pages involved.
For Zola’s Les Rougon-Macquart, it undeniably was.
Let’s walk through this Front Matter implementation.
In the project, novels can have up to 90 characters. Which means, if we were to have novels’ Front Matter list their related characters, we’d end up with a 90 entries array at the top of our .md
file. This is not ideal.
Beside we don’t really need to have our relationship connection refered in both novels and characters.
Characters can have no more than 4 or 5 novels, so we’ll let the characters declare their few novels rather than let the novel declare their many characters.
For the character Eugène Rougon for instance, who appears in 4 novels, we add the following.
title: Rougon (Eugène)
novel:
- argent
- curee
- fortune
- excellence
Now in the novels’ Front Matter, we just need to add an identifier key. For the novel « Son excellence Eugène Rougon » in which stars good old Eugène we add:
title: Son excellence Eugène Rougon
id: excellence
Relationships in our templates
From Eugène’s landing page we’ll need to output the novels he appears in. We can use where
and intersect
to build our list:
{{ $characters := where .Site.Pages.ByTitle ".Params.novel" "intersect" (slice .Params.id)}}
From the novel Son Excellence Eugène Rougon, as we need to ouput its characters, we use where
and in
:
{{ $novels := where .Site.Pages.ByTitle ".Params.id" "in" .Params.novel }}
Done! We successuflly implemented a Many-to-many relationship like it’s 2016!
Now, all of this works great but…
interesect
?where in
? Aren’t we overdoing it a bit ?- 🐌 Built time is roughly 7 times Hugo’s average: ~7 seconds for 1300 pages.
- 💩 It looks ugly.
Oh well… what ya gonna do? 🤷♂️
Nothing… that is until Hugo .27
Enters Hugo’s Related Content
Hugo’s Related Content has been added along Hugo .27 in November 2017.
It has been designed to help themes and projects easily build a « See also » module with maximum control on the algorithm. You can set several factors or indices with their own level of importance. Tags, publishing month, authors, can help build your related content list.
Hands down, it is the best tool to grab pages related to a given one using your very own recipe, and if you don’t already use it to build your Related Posts or Related Products widgets, you should really check its doc and play with it. It’s rad!
In our case though, we don’t need a Related Novels module, we need a robust, consistent and build time friendly relationship. And as it turns out, Related Content offers just that!
Implementing Relationships with Related Content
Declaring our index
First we need to declare our list of indices in our config.yaml
file. Here, we only need one: novel
so…
related:
indices:
- name: novel # The name of the indice, same as Front Matter's .Param key.
weight: 1 # We don't really need this, but omitting it would disable the indice.
includeNewer: true # Here our relationship is timeless! This prevents Hugo from ignoring newer posts.
Proper connecting
Our characters’ Front Matter is fine as it is. After all it already lists its novels under a Param key matching our index name, novel
.
On the other hand, our novels identify themselves with id
, this will not do, they must also match the index name. So in our novel’s Front Matter:
title: Son Excellence Eugène Rougon
novel: excellence # was previously 'id'
Good, our novels and characters are now sharing a common .Page.Param
key matching the name of our newly declared index: novel
.
Related Content in our templates
From within our templates, Related offers several different methods to retrieve the related pages. We’ll cover two of those briefly but you should check the doc to find out more.
.Related will get every related pages of a given page using the indices and weight declared in your config. It takes one parameter: the given page.
.RelatedIndices will get related pages using one or several given indices. First parameter is the given page, subsequent parameters are the indices used.
From our single templates, we will use the .RelatedIndices
method to fetch our related novels or characters. This is to limit the related pages to our novel
index and prevent later added indices like tags or author to interfere with this particular relationship.
From a novel’s single template, like Son Excellence Eugène Rougon, we can now list all its, forgive my french, « personnages » like so:
{{ $characters := where (.Site.RegularPages.RelatedIndices . "novel" ) "Type" "personnage" }}
First param is our page context, the given page, second is our familiar index.
And from a character’s single, like Eugène, all its « romans » :
{{ $novels := where (.Site.RegularPages.RelatedIndices . "novel" ) "Type" "roman" }}
That’s it! We now use Hugo’s Related Content to manage our Many-to-many relationship!
And what did we gain beside a cleaner code ?
🚀 6 seconds! …out of the ~7 we started with…
Build time is now at less than 1.5s.
hugo --templateMetrics
to your heart content. You can even checkout the branch oldRelationship
and 🙄 while it builds.Conclusion
By simply using the built-in Related Content feature of Hugo instead of a DIY ugly patchwork, we slashed the build time by more than 70% and all of this with minimum code change.
There is a tremendous benefit in trying to profit from the amazing power of core Hugo new functionnalities, and this modest article tried to show how easy it was to start using and implementing one of them in your existing projects.
If like me, you think Related Content is the way to go to build complex relationship models in Hugo or if, unlike me, you can think of use cases where they’re really not ideal, let me know in the comments!