Hugo Redirects and Partial Functions

I’ve just added a few features to this site, and wanted to explain how I did it. This is a bit of a grab bag, but it might be useful for fellow Hugo users to see what I did.

Site aliases and redirects

I want to keep URIs working for as long as possible, and if one moves, I want to redirect users from the old location to the new one.

I list all my redirects on my site’s control panel.

Quicklinks are a special kind of redirect that I make for shorter URIs.

They are particularly useful for content that I might print, or that I might type on the commandline. Sort of like a “URL shortener”, except using my site’s not-that-short base domain name. More of a “semi-shortener”, I guess.

A list of quicklinks is available at /q.

Where my redirects are defined

I have redirects defined in a few different places.

  • I use built-in Hugo Aliases. This is especially useful for regular page content that is still in Hugo, just at a new location.
  • I define a site-wide parameter in config.yml called MrlRedirects, which is an array of objects with a from and a to property. This is a nice place for a few site-wide things that are not pages and do not have frontmatter to define an alias in.
  • I define a parameter in the page frontmatter called MrlChildRedirects which is also an array of objects with a from and a to property, where the to must be a child of the current page. I added these because I had some files that do not have frontmatter to define an alias in, but I wanted to define that alias closer to the file itself, rather than putting it in the site-wide config file. Quicklinks to formulae are an example of this.

All redirects generated in one place

With several places to define redirects, I decided I wanted to have a single source of truth for all of them. This was hard and is kind of a hack, but by abusing Hugo .Scratch and partial templates, it turns out you can build a partial template that accepts a mutable scratch variable and modifies it. Though it is a partial, it prints no text to the document that uses it. I learned about this via an excellent post, Approximating Custom Functions in Hugo.

With this primitive, I built a partial I called f_redirects.html. Here it is:

{{/*-----------------------------------------------------------------------------------------------
----Return all redirects from all pages of the site
----
----This is a "partial" that is really a function that modifies a scratch var.
----See also https://danilafe.com/blog/hugo_functions/
----
----To call it, you must pass it a scratch var that you expect it to modify.
----It will return without printing anything to the calling page
----(except some newlines).
----
----See the redirectsTable shortcode for how to call this and deal with the return value.
----
----Why the fucking weird comments?
----Fighting with Go templating inserting surprise newlines + wanting this to be readable.
----*/}}
{{- $allRedirs := .allRedirs -}}
{{- $callerCtx := .callerCtx -}}
{{/*------------------------------------------------------------------------------------------------
----Get redirects out of site configuration
----*/}}
{{- range $redir := $callerCtx.Site.Params.MrlRedirects -}}
{{- $allRedirs.Set $redir.from $redir.to -}}
{{- end -}}
{{/*------------------------------------------------------------------------------------------------
----Find all relevant parameters across all pages of the site */}}
{{- range $page := .callerCtx.Site.Pages -}}
{{/*------------------------------------------------------------------------------------------------
----Process Hugo Aliases
----These are aliases supported by Hugo.
----Hugo generates HTML pages for them that meta refresh to the destination.
----We can do better by using them to generate config files for our HTTP server
----that will cause real HTTP redirects.
----*/}}
{{- range $alias := $page.Aliases -}}{{- $allRedirs.Set $alias $page.RelPermalink -}}{{- end -}}
{{/*------------------------------------------------------------------------------------------------
----Process my MrlChildRedirects
----These are my extensions that allow a page to specify an arbitrary redirect for any child object.
----That is, a page at /foo/index.md could generate one for /foo/file.zip but not /bar/file.zip,
----This is intended for creating redirects for files like images etc that are not Hugo Pages.
----*/}}
{{- range $redir := .Params.MrlChildRedirects -}}
{{- $targetUri := printf "%s%s" $page.RelPermalink $redir.to -}}
{{- $allRedirs.Set $redir.from $targetUri -}}
{{- end -}}
{{- end -}}

Yikes, right? This is why I only wanted to do it once. Imagine needing to change this in more than one place!

Using the list of redirects

Here is an example of how it is called, in a Hugo shortcode I wrote called redirectsTable.html. It generates the table of all redirects in the control panel.

{{/* Return all redirects in an HTML list */}}
{{- $allRedirs := newScratch -}}
{{- partial "f_redirects.html" (dict "allRedirs" $allRedirs "callerCtx" .) -}}
<table class="mrl-simple-table">
  <thead>
    <tr>
      <th>Source</th>
      <th>Destination</th>
    </tr>
  </thead>
  {{ range $src, $dest := $allRedirs.Values }}
  <tr>
    <td><a href="{{ $src }}">{{ $src }}</a></td>
    <td><a href="{{ $dest }}">{{ $dest }}</a></td>
  </tr>
  {{ end }}
</table>

I also wrote a shortcode called quicklinksTable.html, which is very similar, but just generates a the table of only quicklinks, used for /q.

{{/* Return all quicklinks in an HTML list */}}
{{- $allRedirs := newScratch -}}
{{- partial "f_redirects.html" (dict "allRedirs" $allRedirs "callerCtx" .) -}}
<table class="mrl-simple-table">
  <thead>
    <tr>
      <th>Source</th>
      <th>Destination</th>
    </tr>
  </thead>
  {{- range $src, $dest := $allRedirs.Values -}}
  {{- if and (ge (strings.RuneCount $src) 3 ) (eq (slicestr $src 0 3) "/q/") -}}
  <tr>
    <td><a href="{{ $src }}">{{ $src }}</a></td>
    <td><a href="{{ $dest }}">{{ $dest }}</a></td>
  </tr>
  {{- end -}}
  {{- end -}}
</table>

As you can see, this lets me retrive a list of redirects from anywhere I want, but it does not actually do any redirecting. For that, see below.

HTTP redirects

Now that I have a list of all the content to redirect, I need to configure my HTTP server to use it.

Hugo cannot do this natively, however, because it is the HTTP server’s job. One way to bridge this gap is to use Hugo custom outputs to generate HTTP server configuration directives for the redirects.

Native Hugo aliases

Hugo Aliases are browser <meta http-equiv="refresh"> pages that Hugo generates. They are effective for browsers, but not for scripts or bots, and do not produce HTTP redirects.

I’ve had aliases set up for a while on some pages, but I wanted to use real HTTP redirects for them so that they work with scripts and bots.

Netlify _redirects

I host this site on Netlify, which has good support for redirects. In its simplest form, you can place the old path and the new path on a single line, separated by a space, in a file called _redirects in your site’s root directory.

I build these with a Hugo custom output that uses my f_redirects.html partial. That takes a bit of setup.

In my config.yml:


mediaTypes:
  text/netlify:
    delimiter: ""
    suffixes: [""]

outputFormats:
  REDIR:
    mediatype: "text/netlify"
    baseName: "_redirects"
    isPlainText: true
    notAlternative: true

outputs:
  home:
    - HTML
    - RSS
    - REDIR

Once it is configured, I can use my partial function to iterate through all the redirects no matter where I defined them and produce them all in the format Netlify expects. My theme has a layouts/index.redir, containing:

# Netlify redirects. See also:
# - https://www.netlify.com/docs/redirects/
# - https://gohugo.io/news/http2-server-push-in-hugo/
{{ $allRedirs := newScratch -}}
{{- partial "f_redirects.html" (dict "allRedirs" $allRedirs "callerCtx" .) -}}
{{- range $src, $dest := $allRedirs.Values -}}
{{ $src | printf "%-50s" }} {{ $dest }}
{{ end }}

This generates a Netlify _redirects file at the root of the Hugo output directory, instructing the Netlify HTTP server to return the redirects I want.

Redirects with hugo serve

The _redirects file only works when the static site is served from the Netlify webserver – to Hugo, that file is just like any HTML file it generates. This means that, unlike Aliases, redirects will not work in dev mode under hugo serve.

That’s OK, though, because they are either quicklinks intended for easy access to content, or to preserve old URIs for search engines, bookmarks, etc. Both use cases are only useful on the live site anyway. I just have to remember that those links will 404 when in dev mode, but not when deployed.

Success

Now I can define redirects anywhere I want, I can reference them from anywhere I want, I have only written the hacked up code to collect them once, I’m using real HTTP redirects instead of browser-only refresh tags, and it even automatically converts Hugo aliases to real HTTP redirects when deployed.

Everything in the table below should work!

Source Destination
/2011/08/12/defcon19-in-my-day.html /blog/defcon19-in-my-day/
/2011/08/15/defcon19-lanyard-pdp8-part1.html /blog/defcon19-lanyard-pdp8-part1/
/2011/09/01/defcon19-lanyard-pdp8-part2.html /blog/defcon19-lanyard-pdp8-part2/
/2011/11/30/windows-symlinks-pain.html /blog/windows-symlinks-pain/
/2012/05/17/deploying-ssl-certificates.html /blog/deploying-ssl-certificates/
/2012/05/17/mozilla-ssl-nss.html /blog/mozilla-ssl-nss/
/2012/12/05/creating-linux-livecd.html /blog/creating-linux-livecd/
/b/qmkhacks /blog/hack-save-qmk-firmware-source-to-keyboard/
/blog/index.xml /rss.xml
/blog/random-failure-problems-upgrading-ansible-python/) /blog/random-failure-problems-upgrading-ansible-python/
/cgit/cgit.cgi/dhd.git/plain/hbase/.bashrc https://github.com/mrled/dhd/blob/master/hbase/.bashrc
/cgit/cgit.cgi/dhd.git/tree/hbase/Microsoft.PowerShell_profile.ps1 https://github.com/mrled/dhd/blob/master/hbase/profile.ps1
/ergodox-ez-layout /projects/keymap.click/
/ergodox-ez-layout/index.html /projects/keymap.click/
/index.xml /rss.xml
/lability-tutorial https://pages.micahrl.com/lability-tutorial
/lability-tutorial/02-Simple https://pages.micahrl.com/lability-tutorial/02-Simple
/lability-tutorial/03-Debugging https://pages.micahrl.com/lability-tutorial/03-Debugging
/lability-tutorial/04-SimpleExpanded https://pages.micahrl.com/lability-tutorial/04-SimpleExpanded
/lability-tutorial/05-SimpleNetwork https://pages.micahrl.com/lability-tutorial/05-SimpleNetwork
/lability-tutorial/06-NatNetwork https://pages.micahrl.com/lability-tutorial/06-NatNetwork
/lability-tutorial/07-AdDomain https://pages.micahrl.com/lability-tutorial/07-AdDomain
/lability-tutorial/backmatter/concepts/hyperv/default-switch https://pages.micahrl.com/lability-tutorial/backmatter/concepts/hyperv/default-switch
/lability-tutorial/backmatter/concepts/hyperv/switch-types https://pages.micahrl.com/lability-tutorial/backmatter/concepts/hyperv/switch-types
/lability-tutorial/backmatter/concepts/lability/pending-reboot https://pages.micahrl.com/lability-tutorial/backmatter/concepts/lability/pending-reboot
/lability-tutorial/backmatter/concepts/powershell/remoting https://pages.micahrl.com/lability-tutorial/backmatter/concepts/powershell/remoting
/lability-tutorial01-Introduction/01-Introduction https://pages.micahrl.com/lability-tutorial/01-Introduction
/projects/keyblay /projects/keymap.click/
/q/cli.template.py /formulae/pyscript/cli.template.py
/q/qmkhacks /blog/hack-save-qmk-firmware-source-to-keyboard/
/q/template.sh /formulae/shellscript/template.sh
/standing-invitation /invitation/
/talks/livecd.html /blog/creating-linux-livecd/
/unpublished/lets-encrypt-dns-challenges-appliances-behind-firewall/ /blog/lets-encrypt-behind-firewall/
/writing /blog/