Previous and Next Posts in Taxonomies in Hugo
Hugo (the static site generation tool that I use for this blog) is great, but when you have to dive too far into the templating logic, it gets a bit messy. Mostly due to my own unfamiliarity with the templating setup, it took me quite a bit of time to figure out how to get links to the previous and next posts in a series. This will work for all taxonomies (tags, categories, etc.), but is the most natural for a series.
Although this page isn’t in a series (yet, at least), you can see an example of it right below.1 right under the date. Sure, it’s ugly, but that’s because I’m bad at CSS.2
Mostly to increase my own understanding of the intricacies of the code I wrote, I figured I should write this up. Note that this is most likely inefficient, and there’s probably a better way of doing it. But since this step only runs once per push, a little inefficiency isn’t the worst thing.
This post assumes that you’re familiar with basic programming terminology, like scope, loops, and blocks. If you already know Go’s templating,3 you’re already miles ahead of me.
The approach I took is fairly clumsy, and wouldn’t be the approach I’d take in languages that have more robust searching and filtering mechanisms. With all those caveats aside, let’s examine the code. If you’re just interested in the full solution, it’s at the end.
{{ range (.GetTerms "series") }}
...
{{ end }}
.GetTerms "series"
is an operation on the context (the page), getting all of the “series” taxonomies that it belongs to. It’s a list of pages, where each page is the website page for that taxonomy. Notably, it being a list of Page instead of the taxonomy name simplified things greatly.
{{ range (.GetTerms "series") }}
iterates over all of the series that this page belongs to. The ...
is doing a lot of work here, as it contains the rest of the script that we’re writing.
If a page doesn’t belong to any series, this range loop will be skipped.
<!-- Get next page -->
{{ $pageReached := false }}
{{ $next := false }}
{{ range .Pages.Reverse }}
{{ if not $next}}
{{ if $pageReached }}
{{ $next = . }}
{{ else if eq .Permalink $.Permalink }}
{{ $pageReached = true}}
{{ end }}
{{ end }}
{{ end }}
Now we move on to the meat of the script. Go templating declares custom variables using $varName := value
, as opposed to most languages which just use the =
.
Since we’re in the scope of the range, .
now refers to the page for the current series we’re iterating over. And, even more helpfully, .Pages
gives you a list of all pages in that taxonomy. By default, pages are in reverse chronological order, so we need to reverse it to ensure that the page after the page we’re on is the next page.
$pageReached
is set to true when we reach the current page and is used to know when we can snag the next post in the sequence. Likewise, $next
is initialized to false, and will be overridden with the next post if the current post isn’t the last one in the series.
The most confusing part is the check to see if we should assign $pageReached to true, {{ else if eq .Permalink $.Permalink }}
. While .
refers to the current context, $.
will always refer to the page context.
So we’re checking if the post we’re iterating over, .Permalink
, is equal to the post that’s being rendered, $.Permalink
. The permalink is used as a shorthand for equality, since they should only be equal if the pages are the same.4 If that’s the case, we’ve reached our post and the next one is the one we’re interested in.
<!-- Get previous page -->
{{ $pageReached = false }}
{{ $prev := false }}
{{ range .Pages }}
{{ if not $prev}}
{{ if $pageReached }}
{{ $prev = . }}
{{ else if eq .Permalink $.Permalink }}
{{ $pageReached = true}}
{{ end }}
{{ end }}
{{ end }}
This is largely the same as the previous block. Instead of searching through the pages in reverse, we’re searching through them in forward order. Instead of declaring $pageReached
like we did last time, we’re simply assigning it, which just involves replacing the :=
with =
.
<!-- Print series info -->
{{ if (or $next $prev) }}
<div class="series-link">
{{ if $prev }}
<a href="{{ $prev.Permalink }}">{{ $prev.LinkTitle }}</a> ←
{{ end }}
<a href="{{ .Permalink }}">{{ .LinkTitle }}</a>
{{ if $next }}
→ <a href="{{ $next.Permalink }}">{{ $next.LinkTitle }}</a>
{{ end }}
</div>
{{ end }}
This bit is the most specific to what I’m doing with my approach. I only want to display information about a series if there’s more than one post in the series (i.e. not just this one), so I check if either $next
or $prev
exist. In other words, whether they’ve been overridden from their default value of false.
Then I display the previous post if it exists, the taxonomy, and then the next post if it exists. Simple! Or so it looks now. All told, it took me a couple hours to figure it out. Templating is not my strong suit.
And here’s the full code, for reference:
{{ range (.GetTerms "series") }}
<!-- Get next page -->
{{ $pageReached := false }}
{{ $next := false }}
{{ range .Pages.Reverse }}
{{ if not $next}}
{{ if $pageReached }}
{{ $next = . }}
{{ else if eq .Permalink $.Permalink }}
{{ $pageReached = true}}
{{ end }}
{{ end }}
{{ end }}
<!-- Get previous page -->
{{ $pageReached = false }}
{{ $prev := false }}
{{ range .Pages }}
{{ if not $prev}}
{{ if $pageReached }}
{{ $prev = . }}
{{ else if eq .Permalink $.Permalink }}
{{ $pageReached = true}}
{{ end }}
{{ end }}
{{ end }}
<!-- Print series info -->
{{ if (or $next $prev) }}
<div class="series-link">
{{ if $prev }}
<a href="{{ $prev.Permalink }}">{{ $prev.LinkTitle }}</a> ←
{{ end }}
<a href="{{ .Permalink }}">{{ .LinkTitle }}</a>
{{ if $next }}
→ <a href="{{ $next.Permalink }}">{{ $next.LinkTitle }}</a>
{{ end }}
</div>
{{ end }}
{{ end }}
Not too bad, right? As it often ends up, when I started writing this post, the code was considerably more complex. As I understood what each aspect of my previous code was doing, I was able to simplify it.
I’d copied some code from a different blog to get started, and it turned out that person was really taking the long way around to get to the solution.5 After understanding what each thing was doing and, more critically, understanding what things wouldn’t have made sense when the creators of Hugo were designing it, it was simple to find ways to make it cleaner.
But, of course, I’m not infallible myself. I wouldn’t be surprised if there was an easier way to do what I’ve accomplished. Hopefully this will serve as a nice stepping stone as you figure it out for yourself.
-
And, let’s be honest, visual design as well. ↩︎
-
Which is what Hugo uses. ↩︎
-
Otherwise your blog has way bigger issues. ↩︎
-
They were getting the taxonomy, then lowercasing the .LinkTitle to use as an argument into the Site variable to get the list of pages with the tag. Notably, it didn’t work with my taxonomies, which had spaces. Using
urlize
on the .LinkTitle did work, but it was all a runaround to get something that the Page object already had. In their defense, the Hugo documentation can be obtuse at times, and the functionality might not have existed back then. ↩︎