true backlink support in hugo

This commit is contained in:
Wouter Groeneveld 2022-04-18 08:53:05 +02:00
parent 0169b635a1
commit ed6cd5efdd
1 changed files with 66 additions and 0 deletions

View File

@ -0,0 +1,66 @@
---
title: True Backlink Support in Hugo
date: 2022-04-18T08:22:00+02:00
categories:
- webdesign
tags:
- 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](https://jefklakscodex.com/games/), 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.
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](/post/2021/11/from-analog-notebook-to-digital-vault/)-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](https://www.gabrielle-earnshaw.com/posts/implementing-backlinks-in-a-hugo-website/), 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 `x`th 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](https://jefklakscodex.com/games/switch/hollow-knight/) 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!