Repurposing Hugo as a wiki

As part of a project that I’m working on, I decided to see if I could repurpose the excellent Hugo website framework (which also powers this very site) as a single-user wiki of sorts.

Short version: yes!

Requirements

The main features that I wanted were:

Why not <wiki software X>?

The main reason that I didn’t want to use an established wiki package like MediaWiki, the software behind Wikipedia, was that it seemed like overkill. For this project, I don’t need separate authors, I don’t need revisions of pages, and I certainly don’t need a database server.

Hugo exists to create static HTML websites, supports Markdown formatting, is fast, and has a powerful shortcode system which can be extended.

So let’s make it happen. But first, before I get too far into the weeds, let’s take a look at the end result and how simple it is to use while writing.

Using the custom shortcode

I created a custom shortcode called link. Here’s how you use it.

That’s it. But critically: if in the above examples the pages other or other2 don’t exist, the link will be styled in a noticeable way (in my case, red instead of blue) and will link to (also, in my case) /pages/missing/. Whenever I get around to adding said page, all existing links to it will auto-update and become blue in future.

Overview of components

A few things need to happen to bring this configuration together.

How the shortcode works

The shortcode took the most work, as the other bits were just a little configuration tweaking and some strategic web searching in order to puzzle through some minor issues. Here’s how it works.

Line 1: Resolve destination

In the first line, a $link variable is created which benefits from the flexibility of specifying either one or two arguments to the shortcode — one if the page slug and linked text are the same, and two if they differ.

{{- $link := (urls.RelRef . (cond (eq (len .Params) 2) (.Get 1) (.Get 0))) -}}

I will add that spending a lot of time recently in Lisp and other similar languages which are composed of S-expressions has made Go syntax like (cond (eq (len .Params) 2)... much more natural to me.

The best part about the use of urls.RelRef in the link buildup is that the links are filename- and reorganization-independent. If you use a destination page filename of other.md, a shortcode argument of other will find it. If you use a filename of other-page.md but a slug within its front matter of other, it will also work. If you change the location of resources but keep either of these the same, it will be found. And finally, if the page cannot be found (likely because it doesn’t exist yet, one of the requirements of tolerance in this system), the “missing” common page will be linked (in my configuration above, /pages/missing/).

Moving on from the $link setup, the other line of the shortcode sets up a standard HTML anchor link <a> tag highlighting the shortcode’s first argument as the link text. Most importantly, if the link resolves to the common missing page, this can be detected and the custom “doesn’t exist yet” CSS is applied to the link.

<a href="{{ $link }}"{{ if eq $link (urls.RelRef . "missing") }} class="missing"{{ end }}>{{ .Get 0 }}</a>

Astute readers and/or optimization nerds will notice that this technically relies on duplication of the missing term — once here and once in the hugo.yaml part which sets up the missing page for failed urls.RelRef resolution. While I would have liked to avoid this, no Hugo function exists to get at the same of the refLinksNotFoundURL variable specified in the site configuration. The closest I got was .Site.Config, but that only exposes the services and privacy keys as a subset of the configuration. Argh, code duplication.

Exciting conclusion

All in all, I’m quite happy with this. While I’ve only really begun the actual buildout of the site for which this system was contrived, it’s already reducing mental overhead and allowing me to see some structure to the site without having to worry about writing it all or even creating placeholder page files for now.

It’s definitely a case of The General Problem, though.

What can I say? I’m most certainly a developer and toolmaker at heart. I prefer to take inspiration here from Abraham Lincoln as quoted in the previous link:

If I had six hours to chop down a tree, I would spend the first four sharpening the axe.

Bonus: Visual Studio Code snippets

Not content with a mere single level of yak shaving, and in keeping with the spirit of the preceding section, I also wanted a quick way to insert these shortcode-based links into my writing. I learned about VS Code snippets which are both easy and powerful, and I was able to create a project-specific .vscode/snippets.code-snippets file with the following:

{
  "Insert wiki page link": {
    "scope": "markdown",
    "prefix": "link",
    "body": ["{{% link ${1:linked text} ${2:slug} %}}"]
  }
}

Then, in my project, whenever I type link, I can Tab to get a popup suggestion with tab-completion into the two fields for easy fill-out. And if I only want one shortcode argument, I can easily delete the second tabbed-to argument.

Bonus 2: Extra-meta

In the process of writing this post in Hugo and about Hugo, I discovered a puzzler of a problem with including the example shortcode in the usage and snippets examples — the shortcode doesn’t actually exist on this site, but was still being interpreted by Hugo as a shortcode in this post’s content, causing the page render to fail. Bless this kind soul, Chris Liatas, for writing about a technique to work around this using C-style code comments, thereby allowing me to display unparsed Hugo code in a post about Hugo code within a system running Hugo code.


See the discussion about this post on Lobsters.