I’m using Hugo quite a while for generating my website. Hugo creates static html files from markdown and more, which can be hosted nearly anywhere.

For aliases and similars Hugo is using redirects through HTML, basically it sets an header-tag like <meta http-equiv="refresh" content="0; url=https://tilseiffert.de/linktree/">.

But I do not fully like this approach. It’s fast, but it is not the fastest. A faster way would be a HTTP-redirect. But how we could achive this?

As the my hosting server is using Apache, I would like to automatically generate .htaccess-files to set 302-redirects. This would be faster and nicer.

1. Generate .htaccess-files through Hugo

Following this post in Hugo’s Discourse I implemented:

  • Define a custom output format and custom media type

    Add the following to your config.yml:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    outputFormats:
        htaccess:
            baseName: ""
            isPlainText: true
            mediaType: "text/htaccess"
            notAlternative: true
    
    mediaTypes:
        "text/htaccess":
            suffixes: "htaccess"
    
  • Add the newly configured output in your config.yml:

    1
    2
    3
    4
    5
    6
    
    outputs:
        home:
            - HTML
            - RSS
            - JSON
            - htaccess # add this line/option
    
  • Create an empty file index.htaccess in ./layouts/_default

Now with the next build an (empty) .htaccess-file will be generated in ./public (resp. your build-directory).

2. List all aliases as redirects

Next we want to fill the file with life. I am following this guide by micahrl, who was implementing the very same thing but for hosting with netifly. See there for more information of his/her thoughts. This was the trickies part for me, as I was quite long searching for a way to list all aliases in Hugo through a shortcode.

  • Create a new partial template f_redirects.html within ./layouts/partials and fill it with the logic:

     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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    {{/*  Credits: https://me.micahrl.com/blog/hugo-redirects-partial-functions/#all-redirects-generated-in-one-place  */}}
    
    {{/*-----------------------------------------------------------------------------------------------
    ----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 -}}
    
  • Set the .htaccess template in ./layouts/_default/index.htaccess:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    ### Redirects ###
    {{/*
        List all aliases
        Credits: https://me.micahrl.com/blog/hugo-redirects-partial-functions/#using-the-list-of-redirects
    */}}
    {{- $allRedirs := newScratch -}}
    {{- partial "f_redirects.html" (dict "allRedirs" $allRedirs "callerCtx" .) -}}
    # hugo aliases
    {{- range $src, $dest := $allRedirs.Values }}
    Redirect 302 {{ $src }} {{ $dest }}  
    {{- end }}
    

Now with the next build the fully filled .htaccess-file will be generated in ./public. For example on my site this looks something like this:

1
2
3
4
5
6
7
8
9
[...]
### Redirects ###

# hugo aliases
Redirect 302 /affiliate-link /posts/2210-affiliate-links/
Redirect 302 /blog/code-snippets/ssh-keygen /posts/2209-ssh-keygen/
Redirect 302 /blog/technik/meine-buchhaltungs-software /posts/2210-meine-buchhaltungs-software/
Redirect 302 /datenschutz /impressum/
[...]

I also configured some more static stuff in the template like delivery of compressed brotli-files and so on.