In this post I share the backlink functionality I have implemented for my blog. I have seen some other solutions out there, but none of them were complete, functional, and covered so many conditionals as this one I am presenting here. See, for example

The biggest limitations I found were:

  • None of them take into account multilingual sites.
  • None of them take into account the use of Leaf Bundles as pages. In my case, if I have a post with images or any other content, I create a Leaf Bundle to have all the content related to a post in the same folder its content is. However, this means that the name of the file where the content is written is index.es.md, and therefore taking the name of this file means nothing to a backlinking routine.

Please, if you find some other condition under which this routine stops working, let me know.

Backlinks definition

Let’s assume we have a blog with four pages: page 1, page 2, page 3, and page 4. Furthermore, in pages 2 and 3 there are references (links) to page 1. If we are standing in page 1, there are two backlinks, one for page 2 and other for page 3 because they both have links to page 1, they have forward-links. Therefore, a backlink to the current page is any other page which mentions it.

The code

This is the complete code I have made to solve this problem, to have backlinks in my posts. In the forthcoming section can be found the explanation of its parts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- variable re gets the name of the current post -->
{{ $re := "" }}
{{ with .File }}
<!-- If we are in a Leaf Bundle the "index" file means nothing for backlinking, therefore we need the ContentBaseName (see https://gohugo.io/variables/files/#examples) -->
    {{ if (eq .TranslationBaseName "index") }}
        {{ $re = .ContentBaseName }}
<!-- I want to skip the Branch Bundle pages, as they are not made for content and will not have backlinks -->
    {{ else if (eq .TranslationBaseName "_index") }}
        {{ $re = "BranchBundle" }}
<!-- Finally, if it is a regular content page, just take its translational name, without lang extension -->
    {{ else }}
        {{- $re = .TranslationBaseName -}}
    {{ end }}
{{ end }}

{{- $backlinks := slice -}}
<!-- I don't want the backlinks to be searched neither for Branch Bundles, nor for other pages which are not posts -->
{{ if and (ne $re "BranchBundle") (eq .Page.Section "posts") }}
{{- range (where .Site.RegularPages "Section" "posts") -}}
    {{ if (eq .File.TranslationBaseName "index") }}
        {{- if and (findRE $re .RawContent) (not (eq $re .File.ContentBaseName)) -}} {{ $backlinks = $backlinks | append . }} {{- end -}}
    {{ else }}
        {{- if and (findRE $re .RawContent) (not (eq $re .File.TranslationBaseName)) -}} {{ $backlinks = $backlinks | append . }} {{- end -}}
    {{ end }}
{{- end -}}
{{- end -}}

{{- if gt (len $backlinks) 0 -}}
<h1>Backlinks</h1>
<div class="BackLinkDiv">
  <ul id="BackLinkList">
    {{ range $backlinks }}
    <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
    {{ end }}
  </ul>
</div>
{{- end -}}

Getting the name of the current post

In my code, I included something I have never seen before in others’, and it was the reason why no other solution worked for me. The problem arises when we have a stricture like the following in our blog:

content/
|--now/
|  |--_index.en.md
|  |--_index.es.md
|--legal/
|  |--_index.en.md
|  |--_index.es.md
|--posts/
|  |--some-post-here.en.md
|  |--another-post-there.es.md
|  |--leaf-bundle-post/
|  |  |--index.en.md
|  |  |--index.es.md
|  |  |--picture.png
|  |  |--video.mp4
|  |--_index.en.md
|  |--_index.es.md
|--_index.en.md
|--_index.es.md

In this case you need to take into account two things:

  1. There are two languages.
  2. There are Leaf Bundles.

If during the code yo don’t use variables and properties which take into account this characteristics, you will never have a complete solution. It is possible that there are other conditions I have not taken into account here neither… only time will tell. If you find one, please write me back.

  • If you have two or more languages, it is likely that your posts will be named with the language code: post-1.en.md, post-1.es.md, and post-1.fr.md. However, when you make a link in one post to another, you don’t include the language code, as it is Hugo who takes charge of the correct language linking. For this reason we need to make use of the variable .File.TranslationBaseName (which returns post-1 etc.), and not .File.BaseFileName which would return post-1.en etc. The name post-1.en would never be found in the body of any other post.
  • If we are in a Leaf Bundle the index file means nothing for backlinking, therefore we need the .File.ContentBaseName (see https://gohugo.io/variables/files/#examples), which would be in this case the real name of the post. In the example tree above, the .File.ContentBaseName variable of the leaf-bundle-post is not index, but leaf-bundle-post. This is what we are looking for.

Solution

The first thing to do is to get the name of the current post, so we can later test against the content of all other posts and see if it appears there. If there is any post in which the present post is mentioned (with {{ ref "current-post-name" }} in its body), then it means we need to add a backlink in this one (current-post-name) making reference to the mention made in the other post.

First we make a variable $re, and afterwards make use of the correct .File variables:

  1. If .File.TranslationBaseName, that is, the name of the posts without taking into account neither the extension nor the language code, is equal to index, it means we are in a Leaf Bundle. In this case what we need to take as the string to search for in other posts is not index, but the name of the Leaf Bundle. To do that, it is necessary to use .File.ContentBaseName. I have tested it, and it returns, indeed, the name of the last leaf or branch bundle. It is perfect for this use.
  2. If .File.TranslationBaseName is not equal to index, there are two possibilities:
    1. Either we are dealing with a post which has a name, it is regular content. In this case we have just to collect the name of the post and assign it to the variable $re using .File.TranslationBaseName.
    2. Or we are dealing with a Branch Bundle. This should not happen, as the branch bundles are not for content themselves (at least in my use-case). For this reason I just included another if to set the variable $re = "BranchBundle" and skip later every time I see it set to this value.

These conditions can be meet by a code like the following (I think 😉):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- variable re gets the name of the current post -->
{{ $re := "" }}
{{ with .File }}
<!-- If we are in a Leaf Bundle the "index" file means nothing for backlinking, therefore we need the ContentBaseName (see https://gohugo.io/variables/files/#examples) -->
    {{ if (eq .TranslationBaseName "index") }}
        {{ $re = .ContentBaseName }}
<!-- I want to skip the Branch Bundle pages, as they are not made for content and will not have backlinks -->
    {{ else if (eq .TranslationBaseName "_index") }}
        {{ $re = "BranchBundle" }}
<!-- Finally, if it is a regular content page, just take its translational name, without lang extension -->
    {{ else }}
        {{- $re = .TranslationBaseName -}}
    {{ end }}
{{ end }}

Conditions to be meet by a page to have backlinks

The way this functionality is being implemented, because it is embed into each page template, every page of the blog will have this code written in it. For this reason, taxonomy pages, the home page, the search page, etc. will all have a backlink section, and that is not desirable. What we want is to have only backlinks in the posts.

Therefore, we only want to enter the cycle to get the backlinks if we are sure this page we are in must have a backlinks section. It makes no sense to enter the cycle for taxonomy pages, for the home page, etc., and then print nothing: that would consume time for no reason. To achieve this, before entering the cycle, and after having initialized $backlinks to an empty slice, it is necessary to write a conditional selecting for which pages are we interested in having a backlink section. That conditional could be something like this:

1
2
3
{{ if and (ne $re "BranchBundle") (eq .Page.Section "posts") (ne .Page.Kind "taxonomy") (ne .Page.Kind "term") }}
... cycle
{{ end }}

This means that $backlinks will remain an empty slice if:

  1. The current page is a Branch Bundle.
  2. The current page is not in Section posts.
  3. The current page is a taxonomy page, that is, tags and categories.
  4. The current page is a term page, that is, a list of taxonomies (I think).

Notice that the last two conditionals are redundant, as those pages will never have a posts Section type. It is safe to remove that if you wish, and test 😉.

Cycle to get all posts content and search for backlinks

The code first gets the name of the current page, then creates an empty variable (of type slice) called $backlinks, and finally checks if it would be desirable to have a backlinks section in the current page. At this point we are ready to search for the backlinks to this page.

The cycle ranges all pages which are not Branch Bundles (as it makes no sense to me to have a Branch Bundle with content I would like to backlink to) and who’s Section type is posts. In this cycle the content of each posts page is searched for the name of the current page stored in the $re variable . If the name of the current page appears in another one, then it means that we have found a backlink, that there is some other page mentioning this one, and it will be appended to the $backlinks variable.

  1. If at the end of the cycle the variable $backlinks is still empty, it means that either:
    1. There were not backlinks found,
    2. The current page is a Branch Bundle, or
    3. The current page is not a post, and therefore we don’t want to write a backlinks section on it.
  2. If at the end of the cycle the variable $backlinks is not empty, then it means we have found some backlinks. In this case you can range over the backlinks and print them the way you want.
⋇ ⋇ ⋇ ⋇ ⋇

Thanks for reading the post! Do not hesitate to write me an email, and share your point of view 😉: contact@poview.org

Backlinks

Backlinks redirects you to other posts mentioning this one you are in. Just like in Obsidian, Logseq, etc.