6+ years of using Hugo for a tiny website

I spent a bit of my holiday time recently simplifying this site, and it gave me an appreciation for Hugo (the thing that builds the HTML for me). I wanted to write this down while I still remember, so that when I'm re-evaluating the choice of Hugo in another 6 years, I'll have something to refer to.

  % git log --diff-filter=A -- config.yaml
  commit 16e988b07edbf9e914d525e29bc88ba74d6ed466
  Date:   Fri Jan 31 17:51:46 2014 +0800

      Convert site structure from Jekyll to Hugo.

As is usually the way with things that work, I spent some time setting up Hugo then mostly left it alone. In the last couple of years though I was noticing more log lines like this:

  WARN Page.RSSLink is deprecated and will be removed in a future release.
  Use the Output Format's link, e.g. something like: 
      {{ with .OutputFormats.Get "RSS" }}{{ .RelPermalink }}{{ end }}

Somewhere in the six years between v0.9 and v0.74.3 (maybe they'll bump the major version number one day…) some of the internal templating functions have changed. That's entirely reasonable, and Hugo is doing the correct thing by continuing to support the old way, while outputting a "WARN" line. The warning even includes a working example of what you should use instead. ♥️

Some of the other warnings were… less useful:

  WARN found no layout file for "HTML" for kind "taxonomy":
  You should create a template file which matches Hugo Layouts
  Lookup Rules for this combination.

Personally I don't find this message very helpful. "This combination" of what? Thankfully it didn't take me long to figure out that I didn't need to create any mysterious template files, I just needed to add this to my site config:

  disableKinds:
  - taxonomy
  - term

Somewhere along the way Hugo has picked up support for cross-referencing your content with taxonomies. My site was working fine without it, and in this instance the WARN message could have mentioned the disableKinds documentation. Or maybe enableKinds would have been the better way to go.

Overall though I'm happy with Hugo's backwards-compatibility support. It gives me confidence that it will continue to be low maintenance. With other CMS's (over similar spans of time) I've had to contend with database version changes, schema migrations, markdown library changes rendering things differently and probably more that I'm forgetting. With Hugo I downloaded the latest binary, pointed it at the same directory of source files and it worked.


What did I gain from the upgrade? Well, there's some new functionality in Hugo I was keen to take advantage of:

  1. Data templates
  2. Asset pipelines
  3. The built-in deploy command

Previously I was using client-side javascript to fetch things like my recent Pinboard links for the homepage. This worked, but always felt like something that should be done when the site HTML is built. Now, thanks to the getJSON function I can do this:

  {{ $pins := getJSON "https://feeds.pinboard.in/json/u:hjst/?count=6" }}
  {{ range first 6 $pins }}
      <article class="pin">
          <header>
              <h2><a href="{{ .u }}">{{ .d }}</a></h2>
          </header>
          <p>{{ .n }}</p>
      </article>
  {{ end }}

This makes pages faster to load (fewer network requests) and more accessible. It also let me delete ~100 lines of javascript, which was enjoyable.

Most importantly though, I wanted the combination of the deploy command and asset pipes.

Whenever I've written a website I've tended to use a Makefile, and I've usually ended up putting a deploy target in there. I don't particularly like make but you can at least rely on it to be everywhere and to not change.

Then CI/CD pipelines became a thing and now my shoddy Makefile is being run in container images, and I'm trying to remember how it works while debugging a broken build because apparently now this image has a different version of s3cmd, or there's some inconsistency with rsync flags, or YUI Compressor isn't working because JAVA_HOME is being unset in the CI container (but works locally, of course), or many other reasons I'm forgetting.

I replaced all of my own cobbled-together system of find-based concatenation and minification with this:

  {{ $style := resources.Get "css/main.css" | minify | fingerprint }}

Not only does this have considerably fewer moving parts, but I get a cache-busting hash appended as well.

The biggest win though, by far, is the deploy command. With these 14 lines of config…

  deployment:
    targets:
      - name: S3
        URL: 's3://henrytodd.uk?region=eu-west-2'
        cloudFrontDistributionID: E351L92H63NGCK 
    matchers:
      - pattern: ^.+\.(js|css|svg|ttf)$
        cacheControl: 'max-age=31536000, no-transform, public'
        gzip: true
      - pattern: ^.+\.(png|jpg)$
        cacheControl: 'max-age=31536000, no-transform, public'
        gzip: false
      - pattern: ^.+\.(html|xml|json)$
        gzip: true

…I get less error-prone gzip'ing (my find -exec gzip patterns had a bug I missed for like 2 years) and CloudFront cache invalidation. Yes, it only supports a blanket /* invalidation, but that's understandable.

Now the only dependency for building and deploying the site is Hugo itself. This makes my Gitlab CI/CD config pleasingly simple:

  image:
    name: klakegg/hugo:0.74.3-busybox
    entrypoint: [""]

  production:
    type: deploy
    script: "hugo && hugo deploy --verbose"

Hopefully it will run undisturbed for another 6+ years.