Emacs, scripting and anything text oriented.

Parsing Backlinks in Hugo

Kaushal Modi

A Hugo partial to parse all the backlinks to any post from the same Hugo-generated website.

If a post is referred to in other posts, then all those posts are creating backlinks to that first post.

As of writing this (<2022-04-19 Tue>), Hugo doesn’t have a built-in way to generate a list of such backlinks, though there’s an open issue (# 8077) to track this feature request.

One way to gather a list of backlinks to a post is to find out that post’s relative or absolute permalink, and search for the occurrences of that link in all the other posts on the published site. The author of seds.nl: Export org-roam backlinks with Gohugo, Ben Mezger, uses this approach in his solution for creating backlinks in that post.

In this post, I am expanding upon that solution and refactoring it bit to fit my needs.

Code #

Without further ado, here is my version of the partial:

Save this partial to your site repo as layouts/partials/backlinks.html.

{{ $backlinks := slice }}
{{ $path_base := .page.File.ContentBaseName }}
{{ $path_base_re := printf `["/(]%s["/)]` $path_base }}

{{ range where site.RegularPages "RelPermalink" "ne" .page.RelPermalink }}
    {{ if (findRE $path_base_re .RawContent 1) }}
        {{ $backlinks = $backlinks | append . }}
    {{ end }}
{{ end }}

{{ with $backlinks }}
    <section class="backlinks">
        {{ printf "%s" ($.heading | default "<h2>Backlinks</h2>") | safeHTML }}
                {{ range . }}
                    <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
                {{ end }}
{{ end }}
Code Snippet 1: backlinks.html Hugo partial

2 Use the partial #

Add a call to this partial in your “single” layout’s template file, which is typically layouts/_default/single.html.

{{ partial "backlinks.html" (dict "page" .) }}
Code Snippet 2: Calling the backlinks.html partial

Features and Improvements #

  1. ✨ The partial now accepts a dict or a dictionary with keys page and heading.
    • The page key is required to pass the page context from where the partial is called.
    • The heading key is optional. This can be used by the user to set the “Backlinks” heading differently. For example, {{ partial "backlinks.html" (dict "page" . "heading" "<h4>Links to this note</h4>") }}.
  2. 🐛 Line 2: .File.BaseFileName would be just “index” for all the Leaf Bundles  If you follow this link and scroll to the bottom of that page, you will see a “Backlinks” section auto-generated with the help of this partial. This post will be linked there because I just referenced that post here. , and I use them heavily! Using .File.ContentBaseName fixes this problem. See 📖 Hugo File Variables for more info. [Credit: @sjgknight]
  3. 🐛 Line 3: Reduce false matches for backlinks by making the regular expression a bit stricter. Now it will match only if the derived $path_base variable is found wrapped in characters like ", /, ( or ).
    • If $path_base is ‘hello’, I don’t want its mere reference like in this line to create a backlink on that ‘hello’ post!
    • Instead, a match will happen only if something like "hello" (as in {{< relref "hello" >}}), or /hello" (as in {{< relref "/posts/hello" >}}), or /hello) (as in [Hello](/hello)) is found in the raw Markdown content (.RawContent).
  4. Line 5: Look for backlinks only in site.RegularPages. See 📖 About site Pages variables for more info. site.AllPages includes all pages.. even the list pages like section and taxonomy pages which, I believe, won’t contain backlinks.
  5. ⚡ The findRE in line 6 is slightly optimized by quitting the search immediately as soon as the first match is found.
  6. 💄 Rest of the changes are just using a different style of coding using Hugo templates and creating a different structure in HTML.

Closing #

This partial works great for this site — adds only a few hundred milliseconds to the build time.

But it’s not an efficient solution. The partial is called in the single template where it searching for backlinks to the current page in all other regular pages, and the single template is evaluated for all the regular pages. So its \(O\) notation will be close to \(O(n^2)\) where \(n\) is the number of regular pages.

I have only about a hundred regular pages at the moment, but I can see this partial taking a major chunk of the build time  The hugo --templateMetrics --templateMetricsHints command prints a table listing all the partials used in the build and how much time each of them took. See 📖 Hugo Build Performance for more info. as the number of pages increase.

A built-in support for backlinks from Hugo (# 8077) would really help in this performance department.