skip to main content

True Backlink Support in Hugo

published icon  |  category icon webdesign

tags icon hugo

A great blog engine removes friction and pushes you to write more. An average blog engine has you sweating and coming up with needless details, such as tags and other metadata. For example, on my retro gaming site Jefklak’s Codex, I categorize each game entry by platform—a platform metadata key that had to be manually entered. After thinking things through and a thorough refactoring attempt, this is now automatically deduced based on the folder name (e.g. /games/switch/unpacking). One less thing to worry about. I’ve written before about reducing friction and automating metadata keys and how it facilitates writing.

The same is true for tags, which traditionally are used in Hugo-powered blogs to find related articles. On the bottom of each Brain Baking post, the following code finds the first three related posts based on the intersection of tags, excluding the current page:

{{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}

This of course only works if you dutifully—and correctly—fill in the tags metadata key. This works well enough for Brain Baking, but not for Jefklak’s Codex, since I wanted to display similar games that link to the current one, or where the current one links to. These are called backlinks and forwardlinks and are very popular with Zettelkasten-like digital note tools like Obisidan and Zettlr. A backlink enables linking blog entries in a natural way instead of coming up with arbitrary tag entries—which always end up in a mess: sometimes I use gameboy, sometimes Game Boy, and sometimes gb.

Others, such as this method here, have “implemented” backlinks by adding more metadata to point to the correct linked article, instead of less! That’s simply ridiculous and adds instead of reduces friction. Yet, true backlinks in Hugo is a piece of cake thanks to the incredibly fast Go engine. The idea is very simple:

  • Loop through all relevant pages
    • If a relative link to the current page is found, it’s a backlink. Collect it.
    • If a link on the current page to that one is found, it’s a forwardlink. Collect it.
  • Concatenate both lists
  • Display the first x to show related articles

This translates into the following Hugo shortcode:

{{ $currRellink := substr .RelPermalink 0 -1 }}
{{ $currContent := .Content }}
{{ $backlinks := slice }}
{{ $forwardlinks := slice }}
{{ range (where (where .Site.Pages ".Section" "in" (slice "articles" "games")) ".Params.ignore" "!=" "true") }}
    {{ $found := findRE $currRellink .Content 1 }}
    {{ if $found }}
      {{ $backlinks = $backlinks | append . }}
    {{ else }}
      {{ $rellink := substr .RelPermalink 0 -1 }}
      {{ $found = findRE $rellink $currContent 1 }}   
      {{ if $found }}
        {{ $forwardlinks = $forwardlinks | append . }}
      {{ end }}
    {{ end }}
{{ end }}

<h3>Related Articles</h3>
{{ $related := append $backlinks $forwardlinks }}
{{ range first 5 $related.ByDate.Reverse }}
  {{ .Title }}
{{ end }}

A few pointers:

  • I pinch off the ending / from the .RelPermalink. Why? Because the permalink is something like /games/switch/toem/, but when I link it in an article, I usually leave out the trailing slash, resulting in no match.
  • I keep backlinks and forwardlinks in a separate list and merge them in the end. Why? Because I want to show the relevant backlinks first, but if there are none, I’ll be content with a few regular links to related games.
  • findRE has a third parameter which helps stopping to look for more matches after the xth one. Since we are only interested in “any” match, set it to 1.
  • Sorting (.ByDate.Reverse) should be added in the end, not in the beginning: that list is much smaller.

The result can be viewed in the sidebar of for example the Hollow Knight review under “Related Posts”, which has links to The Messenger, Castlevania: Aria of Sorrow, Guacamelee, Castlevania Advance Collection, and Super Metroid—all similar games that are not linked by manually adding tags but automatically by collecting backlinks!

One obvious disadvantage is that, if you don’t mention one game in the article of the other (or the other way around), the articles will still not be linked, since we search for the (relative) URL, not the name of the game. That is, implicit backlinks are ignored, a feature of Obsidian that might or might not be of interest. It’s not difficult to adapt the above code to also look for implicit links, but it reintroduces semantic difficulties (will “Castlevania” do or is “Castlevania: Aria of Sorrow” needed, and what about that colon?).

I assume the general idea is easy to implement in other static site generators. If you like pottering around in custom blog engine code, just remember that reducing writing friction is the goal here!

I'm Wouter Groeneveld, a level 36 Brain Baker, and I love the smell of freshly baked thoughts (and bread) in the morning. I sometimes convince others to bake their brain (and bread) too.

If you found this article amusing and/or helpful, you can buy me a coffee - although I'm more of a tea fan myself. I also like to hear your feedback via Mastodon or e-mail. Thanks!