Using Hugo as a redirect service

I have been building my website with Hugo since early 2021. I love the control it gives me. I recently wanted to start using short URLs in presentations, that would link to either a longer URL on the website or to somewhere else altogether.

It turns out Hugo makes this easy but not entirely obvious, so I thought I would write it up.

tl;dr

For local redirects, use aliases. For external redirects, use this template.

Local redirects

These are the easy ones. Hugo supports aliases in a page’s front matter or header, which generate a redirect page. It took a while to find them; it would be great if they called them redirects!

I recently moved all my old blog posts to be top-level pages rather than structured by date, so a URL like /2022/02/10/some-post/ became simply /some-post/.

To prevent any links to the old structure failing, I introduced an alias to each page that looks like this:

/content/blog/some-post.md:

---
title: Some Post
aliases:
- "/2022/02/10/some-post/"
...
---

This causes Hugo to render a page at the old address that looks like this:

<!DOCTYPE html>
<html lang="en-GB">
  <head>
    <title>https://dannorth.net/some-post/</title>
    <link rel="canonical" href="https://dannorth.net/some-post/">
    <meta name="robots" content="noindex">
    <meta charset="utf-8">
    <meta http-equiv="refresh" content="0; url=https://dannorth.net/some-post/">
  </head>
</html>

This establishes a permanent redirect to the new location and means that I don’t “break the web”. I like that they think about things like canonical links, and stop the old page being indexed (the robots meta tag).

Shaving some yaks

Of course I was not going to do this by hand for around 100 posts, so I wrote something to extract the date from the post and insert the alias line. It turned out that many of the older posts had a Unix-style date format, like Sat Oct 21 11:56:44 BST 2023, so naturally I felt compelled to write a little programme to convert all of these to ISO yyyy-mm-dd dates so I could generate the aliases.1

Then I realised that I also needed to know the name of the file that Hugo had rendered the page to, which determines its URL. Rather than try to reproduce Hugo’s permalink mangling logic for title / filename / slug / date etc., I cheated! I rendered the website, then searched for the file containing the post’s title: <meta property="og:title" content="Some Post" />

This all took a few hours’ fun hacking in Go, test-driven of course, and ended up with a single-use utility that would fix the date, figure out the page URL and drop the alias into the file for each post.

Shortening URLs

So now I had my mechanism. I’m using the template /go/shortcut, so on a slide, I might have https://dannorth.net/go/63 to point to my Rule of 63 post at https://dannorth.net/2023/05/09/seek-first-to-understand/.

At this point, I no longer need TinyURL or a vanity domain to point to long URLs on my own site.

External redirects

These are a little trickier, but we can effectively use the same mechanism. Hugo uses an internal template to render the redirect page. For some reason, the alias template is not documented, but it is there.

You can set a custom template for a page by giving it a type property. Hugo will look for the template in layouts/[type]/single.html. The disadvantage with copying an internal template like this is that you lose any updates to the template in future versions of Hugo, which I was not happy about.

The internal alias template uses the .Permalink property of the page object, which it turns out is not editable. You can customise the part after the site’s base URL, but nothing more. Then I realised that I could just pass a custom dictionary into the template, so my final template is a single line!

The solution

/layouts/redirect/single.html:

{{- template "_internal/alias.html" (dict "Permalink" .Params.target) -}}

and the source page now looks like this:

---
type: redirect
target: https://duckduckgo.com
---

which could not be much simpler.


  1. While writing this up, I discovered that zsh has a builtin strftime function, which might have been much quicker! ↩︎