Gohugo and nginx config generator

31 Aug
2023

The gohugo is being called a content management system, because… it allows to manage a content. As someone who used wordpress for far too long I found that gohugo was missed my expectations a bit. One of areas where I found it shortcomings are redirects and aliases.

Gohugo supports aliases which can be defined in frontmatter section (through “aliases” key), however their usage results in generation of extra html files with meta-refresh html header. Its clever way, which works for everyone.

To be fair, when I saw support for _redirects file I hoped that someone did heavy lifting and brought similar functionality for nginx deployments. I was wrong. 🙂 The _redirects file is specific to netlify. Additionally, nginx does not like configuration snippets like Apache2 do (with .htaccess files). So I was forced to make it myself.

Nginx config generator

Gohugo supports alternative output formats of your webpage. It might be a sitemap, rss feed or something imaginary you need. Everything is generated from template files, so it can be pretty much anything, as long as it is a text. In order to bring new format there few steps needed. First of all, it must be declared in configuration, then it needs to be assigned to specific part (home, section, page), and then properly templated.

Because I wanted my format to result in “nginx.conf” file generation I had to declare not only output format but also media type.

mediaTypes:
  "text/nginx-conf":
    suffixes: "conf"

outputFormats:
  nginx:
    isPlainText: true
    mediaType: text/nginx-conf
    baseName: nginx
    notAlternative: true
    noUgly: true

This means that our template will be called <section>.nginx.conf. Since we need only one configuration snippet its necessary only at home level:

outputs:
  home:
    - html
    - rss
    - nginx

Final point is home.nginx.conf template, which turned to be quite simple:

# Page aliases
{{ range .Site.AllPages }}
  {{- if (isset .Params "aliases") -}}
  {{- $page := . -}}
  {{- range .Params.aliases -}}
  rewrite ^{{ . }}$ {{ $page.RelPermalink }};
  {{ end }}
  {{- end -}}
{{ end }}

Above template uses alias and redirects it to target page. The end result is obvious – the HTML file generated by hugo is not being used, which is ok.

Nginx configuration update

In order to make use of generated snippet we still need to update nginx configuration itself. I decided to use a wildcard inclusion above public dir where HTML files are being stored. In below example site is hosted from /var/www/domains/test.gohugo/web directory and configuration is loaded from /var/www/domains/test.gohugo/*.conf files. Since we share there only redirects it should be still ok-ish from security point of view (after proper permissions setup!).

All in all nginx host configuration is as below:

server {
    server_name test.gohugo;

    root /var/www/domains/test.gohugo/web;

    access_log /var/log/nginx/test.gohugo.access.log;
    error_log /var/log/nginx/test.gohugo.org.error.log;

    index index.html index.htm;
    charset utf-8;

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    include /var/www/domains/test.gohugo/*.conf;

    location / {
        try_files $uri $uri/ =404;
        autoindex off;
    }
}

Proxied redirects

One additional thing which I wished to test was redirects which do not expose target URI. This is one of common practices which can be used with full blown CMS systems where i.e. binary attachments are streamed or served through proxy endpoint. Nginx can do it, so why not making an extra data file for that?

I started from basic data file called data/redirects.yaml:

redirects:
  - title: "Connectorio Bindings Manual"
    uri: "/cn-bind-manual"
    target: "https://myimportantserver.com/wp-content/uploads/some-secret-resource.pdf"
    proxy: true

The missing spot is just an extra section to our home.nginx.conf template file. 🙂

# Custom redirects
{{ range .Site.Data.redirects.redirects }}
  # redirect for {{ .title }}
  {{- if (.proxy | default false) }}
  location ~ ^{{ .uri }}$ {
    {{- $url := urls.Parse .target }}
    rewrite ^{{ .uri }}$ {{ $url.RequestURI }} break;
    proxy_pass {{ $url.Scheme }}://{{ $url.Host }};
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  }
  {{ else }}
  rewrite ^{{ .uri }}$ {{ .target }};
  {{- end }}
{{ end }}

The generated config fragment will ensure that 1) proxy directive is properly set up, 2) all necessary headers are set, 3) target file remain hidden. If redirect entry is not set as proxied, it will be simply rewritten by nginx. See:

  location ~ ^/cn-bind-manual$ {
    rewrite ^/cn-bind-manual$ /wp-content/uploads/some-secret-resource.pdf break;
    proxy_pass https://myimportantserver.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  }

Summary

It turns out that with a bit of struggle, it is possible to bring netlify’s features to basic nginx deployment of hugo. What gave me most of the pain was initial setup of media type and output formats. Once I found a working combination of output format, media type and template name whole thing began to work.

I’m going to work a little bit more on this stuff and see if it will fly!

Comment Form

top