Getting Started With Hugo: Series taxonomy and navigation
I wanted to introduce a support for “series” ‒ an ordered collection of posts about the same topic. For example, this post is the 4th post in the series “Getting Started with Hugo”. And I wanted to enable navigation between different posts from the same series instead of just going to “next” or “previous” post in the blog. I looked on internet and found many articles about how to do it using Hugo. The posts that I found used older versions of Hugo because of configuration parameters and methodology that they used. I learned from these posts and came with my own implementation.
Taxonomies
The way to introduce a new thing such as “series” with Hugo is to use taxonomies.
Tags are implemented this way.
One thing to remember when defining new taxonomies in Hugo is that the new taxonomy configuration replaces the default one.
I still wanted to keep tags so my new configuration (in hugo.yaml
) looks like:
taxonomies:
series: series
tag: tags
Now I have two taxonomies: tags
and series
. And I can utilize Hugo functions to navigate around my new series taxonomy.
Like the tags, defining a serie for a post is a matter of simple definition of the serie in the post metadata. In opposite to the tags, one post can belong to only one serie. I know that it is not necessary so, but this is how I intend to use it in this blog. This post metadata looks similar to this:
---
title: "Getting Started With Hugo: Series taxonomy and navigation"
date: "2024-04-13T12:00:00-07:00"
tags: ["hugo"]
series: "getting_started_with_hugo"
---
Build a pager for series
Next I wanted to enable a reader to navigate between posts of the same series.
The idea is to take the current pager implementation and customize it to work for the series
taxonomy.
The default navigation in the Mainroad theme shows the previous and next posts in the same category (i.e. folder or type).
To customize the behavior of the theme, I followed the Hugo instructions.
(A) Customize single.html
template
I copied the theme’s single.html
from /themes/mainroad/layouts/_default/
to my blog’s layouts/_default
folder.
Then I copied the theme’s partial template for page, pager.html
from /themes/mainroad/layouts/partials/
to layouts/partials
folder.
And I renamed the file to be single_pager.html
to distinguish it from the original pager of the theme.
If you use the same theme and do not need other customizations, you can simply copy the pager.html
to layouts/partials
and customize it.
I went to customize the default template for display of the post because I wanted to do additional customizations.
Then I modified the line (line 26 in my case) like this:
remove <<<< {{ partial "pager.html" . }}
insert >>>> {{ partial "series_pager.html" . }}
Running it with hugo serve
I could see that I still get the default pager behavior when I am browsing a post in my blog.
(B) Implement series pager
There is no built-in method for navigating between posts of the same taxonomy in Hugo.
At least I did not find something like NextInSection
function.
Instead, I utilized the extensive template support based on Golang template.
The implementation does it in ??? steps:
- Get all posts with the same
series
taxonomy as the current post. - Order the posts according to their creation date since it suppose to be the natural order of the posts in the series.
- Find the postion (aka
index
) of the current post within the ordered posts. - Show “Prev” link, pointing to the
index - 1
post ifindex
is greater than 0. - Show “Next” link, pointing to the
index + 1
post ifindex
is less than the total number of posts minus 1.
Get all posts in the series (1)
There are several methods to collect the posts that belong to the same taxonomy.
I went with the method from the listing posts with the same taxonomy example that uses get
method:
{{ $series := (.Site.Taxonomies.series.Get .Params.series) }}
Order the posts by creation date (2)
Hugo has sort
method that sorts the collection by the property.
Combined with the call to get
it returns the ordered list of the series posts:
{{ $series := (sort (.Site.Taxonomies.series.Get .Params.series) "Date") }}
Finding the position of the current post (3)
Using the template script I can traverse the collection of the posts from (2) and compare the permalink
property of each post vs. the current post’s value.
The template reminds a simple loop in any of the programming languages.
Because the context in the loop changes, I need to “remember” the permalink of my current page first:
{{ $my_permalink := .Permalink }}
{{ $my_index := -1 }}
{{ range $index, $page := $series }}
{{ if eq $my_permalink $page.Permalink }}
{{ $my_index = $index }}
{{ break }}
{{ end }}
{{ end }}
Rendering the “Prev” link (4)
Afer finding the position of the current post in the collection I could reuse the rendering logic from the original pager.html
implementation to display the internationalized “Prev” and “Next” links.
The following snippet shows the template script that renders “Prev” link.
{{ if gt $my_index 0 }}
{{ $page := (index $series (sub $my_index 1)) }}
<div class="pager__item pager__item--prev">
<a class="pager__link" href="{{ $page.RelPermalink }}" rel="prev">
<span class="pager__subtitle">« {{ T "post_nav_prev" }}</span>
<p class="pager__title">{{ $page.Title }}</p>
</a>
</div>
{{ end }}
Rendering “Next” link is implemented very similar.
The full implementation of the series pager
This captures the full implementation of the series pager.
The pager is rendered only if the post has .Params.series
metadata.
{{ if .Params.series }}
<!-- Find page index in the -->
{{ $my_permalink := .Permalink }}
{{ $my_index := -1 }}
{{ $series := (sort (.Site.Taxonomies.series.Get .Params.series) "Date") }}
{{ range $index, $page := $series }}
{{ if eq $my_permalink $page.Permalink }}
{{ $my_index = $index }}
{{ break }}
{{ end }}
{{ end }}
<!-- Render -->
<nav class="pager flex">
{{ if gt $my_index 0 }}
{{ $page := (index $series (sub $my_index 1)) }}
<div class="pager__item pager__item--prev">
<a class="pager__link" href="{{ $page.RelPermalink }}" rel="prev">
<span class="pager__subtitle">« {{ T "post_nav_prev" }}</span>
<p class="pager__title">{{ $page.Title }}</p>
</a>
</div>
{{ end }}
{{ if lt (add $my_index 1) ($series | len) }}
{{ $page := (index $series (add $my_index 1)) }}
<div class="pager__item pager__item--next">
<a class="pager__link" href="{{ $page.RelPermalink }}" rel="next">
<span class="pager__subtitle">{{ T "post_nav_next" }} »</span>
<p class="pager__title">{{ $page.Title }}</p>
</a>
</div>
{{ end }}
</nav>
{{ end }}
Voilà, I have the navigation between the posts in the same series now.