March 13, 2023

Earlier today I was working on a new post – and yes, I’m actually writing new content, and not just about Hugo!

But that’s not the point.

The point is, I came across an annoyance, and managed to also find a fairly simple solution, which I thought I’d share for those interested.

What is it, you ask?

How about an easy way to link to other local documents without using a shortcode?

The Problem

Sometimes I like to reference other articles. The fastest way to do this with Hugo (out of the box, at least), is via the ref and relref shortcodes, so your content ends up looking something like this:

[reference other articles]({{< relref "02-paged-posts-with-hugo.md" >}})

The issue here is that you have to remember your filenames; as far as I can tell, these shortcodes don’t search on slug. If you do anything clever (as in my case, where I’m using numeric prefixes to force document sorting in directory listings) then you end up having to go look things up every time you want to reference them. It’s annoying.

Not to mention, error prone.

In my setup, I’m using document slugs to manage the URLs, which I assume is fairly common. What I would really like to do, is to reference the document by slug – without using a shortcode – and have Hugo do the rest.

So why can’t I?

The Solution

Fortunately, Hugo gives us the ability to intercept the render pipeline for certain objects and substitute our own renderer. For example, here is my solution to the above problem, as a link renderer stored in my theme as layouts/_default/_markup/render-link.html:

{{- $link := .Destination -}}
{{- $slug := strings.TrimPrefix "::" .Destination -}}
{{- if hasPrefix .Destination ":" -}}
{{-   $pc := where site.Pages ".Params.slug" $slug -}}
{{-   if lt (len $pc) 1 -}}
{{-     errorf "ERROR:  Unable to find slug [%s] for link." $slug -}}
{{-   end -}}
{{-   $p  := index $pc 0 -}}
{{-   $link = relref (site.GetPage "/") $p.File.Path -}}
{{- end -}}
<a href="{{ $link | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text | safeHTML }}</a>

Put simply, any link that starts with a colon will now be re-built by this fragment. If it’s a double-colon, it will look it up as a slug, and use the permalink of the resulting page (relative to the homepage) in the link. Piece of cake!

Why a double-colon? Because that way I can later extend things by using :slug: or :page: in order to restrict the namespace if I want. I just don’t have any need to do that right now, so I’m not bothering. I don’t need to implement the future, but leaving hooks for it is a good thing!

And now I have a simpler, more wiki-like experience:

[reference other articles](::paginated-posts-with-hugo)

YMMV, Of Course

I just threw this together literally ten minutes ago. So far it looks like it will streamline my writing experience considerably. The easier it is to write (and reference), the happier I am with the switch to Hugo.

There are probably better ways (or “best practices”) for Hugo, but if so, then I haven’t found them yet…

Hopefully this will help someone out there make their own experience that much better. The only downside I see for this is if I switch off of Hugo (or remove my link renderer), but I just don’t see that happening any time soon!