A few months ago; I switched this blog over from Jekyll to Hugo, and in the process, built a new theme.

I like building blog themes! Front-end HTML/CSS development is sorta how I got into the whole “programming” thing, and every couple years when I come back to it, I’m blown away how much the technologies, ideas, and aesthetics have changed1.

I don’t want to talk so much about the features of the new design today – I don’t think it’s particularly remarkable, and, like, you’re seeing it now! – but there’s a few features that I’ve built into the Hugo theme that I think are novel and useful, and I wanted to share those here in case they’re useful to anyone else.

They are:

The source for the theme and therefore for the examples below is available on Github.

A mechanism for excluding source images after they’ve been processed or resized

Hugo supports processing and resizing images as part of the build process. When you process an image, the original is left intact in the output directory alongside the processed image. This is done because Hugo can’t detect if the original is used or not, and in Hugo, there’s a general design pattern of favouring simplicity and predictability over fancy extra processing steps (I think).

Some of my blog posts, however, contain photos that are > 5MB in original (e.g this one, about fixing light leaks in film cameras), and rather than push huge files to the production site or resize them to fit the dimensions of the post, I built a theme-specific way of managing this.

Usage

Here’s how to use it:

  1. Whenever you’re working with a very large image, you embed it in your document body using the {{< fitfigure >}} shortcode, which accepts all the same parameters as {{< figure >}}, but also takes care of resizing the image to fit the width of the theme.

  2. After building your site for deploy (i.e. by running hugo), run:

    themes/capnfabs-lite/buildscripts/drop-resources.py public

    … which will take care of excluding the original image resources that were used with {{< fitfigure >}}.

But how does it work?

Under the hood, the {{< fitfigure >}} shortcode generates resized images, and then adds the originals to a Scratch variable for the page named droplist:

{{- if not (.Page.Scratch.Get "droplist") -}}
    {{- .Page.Scratch.Set "droplist" (slice) -}}
{{- end -}}
{{- .Page.Scratch.Add "droplist" $original.RelPermalink -}}

The config.toml for the theme defines an additional content type, called droplist, and requests that a droplist is rendered for every page, in addition to the HTML:

[mediaTypes]
  [mediaTypes."text/droplist"]
    suffixes = ["droplist"]

[outputFormats.droplist]
mediatype = "text/droplist"
isPlainText = true

[outputs]
page = ["HTML", "droplist"]

When Hugo builds the page, it produces both an HTML file and a corresponding .droplist file. For example; for the input file content/posts/example/index.md, Hugo produces both:

The layouts/_default/single.droplist template tells Hugo to build each of these droplists by reading back from the droplist Scratch variable that we populated in the shortcode:

{{ with (.Page.Scratch.Get "droplist") }}
    {{- range (. | sort | uniq) }}{{.}}
{{end -}}{{end}}

This produces an output where each line represents a path from the base output directory to a high-resolution original:

/posts/example/R1-05171-0033.jpg
/posts/example/R1-05433-014A.jpg
/posts/example/R1-05433-021A.jpg
/posts/example/R1-06048-000A.jpg
/posts/example/ROFL6275.jpg
/posts/example/ROFL6276.jpg
/posts/example/ROFL6277.jpg
/posts/example/film-sample.jpg

Finally, the drop-resources.py script:

I’m pretty happy with this solution – while it was a little fiddly to set up, I’ve found that the user experience when writing content has been really straightforward, and hasn’t gotten in my way at all. I haven’t had to think about whether to keep a resource or not; it’s just worked.

Visual differentiation for drafts

I’ve been on a big kick for the last few weeks about the power of visualising what’s changed. In addition to building a tool to check what’s changed in my Hugo build output, I also built a styling mechanism into this theme to visually draw attention to drafts, which makes it really easy to trace a new post from its source file through to the various HTML pages it appears in.

I’ve styled drafts on list pages:

Special background for draft posts on list-style pages

… and on single post pages:

Special background for draft posts on single-post pages

(Note the stripey page background)

… and even on taxonomy pages!

Special background for draft posts on taxonomy pages

This was mostly really easy to add to the theme; I basically just sprinkled {{if .Draft }}draft{{ end }} to a bunch of class definitions in the HTML templates. For example, in layouts/_default/list.html:

{{ range .Data.Pages.ByDate.Reverse }}
    <li class="{{if .Draft }}draft{{ end }}">
        <a href="{{ .RelPermalink}}">{{ .Title }}</a>
    </li>
{{ end }}

… and in layouts/posts/single.html:

<body class="{{ if .Draft }}draft{{ end }} look-sheet-bkg">
...
</body>

The only slightly trickier one was getting the taxonomy pages right, because it has an explicit count of drafts rolled up under the term.

In layouts/taxonomy/terms.html:

{{ range .Data.Terms.ByCount }}
    {{ with .Page }}
        {{ $numDrafts := len (where .Pages ".Draft" true) }}
        <li class="{{ if gt $numDrafts 0 }}draft{{ end }}">
            <a href="{{ .RelPermalink}}">
            <span class="post-title">#{{ .Title }}</span>
            <br>
            <span class="post-meta">
              {{len .Pages}} {{if ne (len .Pages) 1}}entries{{else}}entry{{end}}
            </span>
            <br>
            {{ if gt $numDrafts 0 }}
              <span class="post-meta draft">
                Including {{ $numDrafts }} {{if ne $numDrafts 1}}drafts{{else}}draft{{end}}
              </span>
              <br>
            {{ end }}
            </a>
        </li>
    {{ end }}
{{ end }}

Then, style everything with one line of CSS:

.draft {
  /* Orange stripes? Hell yeah, orange stripes! */
  background: repeating-linear-gradient(
    135deg, #fffdf7, #fffdf7 10px, #ffd4a3 12px, #ffd4a3 12px);
}

It’s probably worth adding your CSS first, so it’s easy to tell when changes are sticking. Start with something like outline: 1px solid red and then you can make it nice later ✨

Checking for Golang template errors

At some point, while developing the source image exclusion feature, I noticed that it had suddenly stopped displaying high-res versions of images on my phone screen. What had gone wrong? Was I going crazy?

Opening the Firefox Dev Tools showed something in my page’s HTML that I definitely wasn’t expecting:

Hmmm, what happened to the srcset attribute here?

Hmmm, what happened to the srcset attribute here?

When Go’s templating engine detects something that it thinks is unsafe, it substitutes the requested content for ZgotmplZ instead of returning an error. This behaviour is kind of annoying – I’d much rather that it hard fail – but fortunately, we can add a step to the build script which checks for these strings.

Here’s the script that builds my site now2:

#!/usr/bin/env bash
# Crash on errors
set -e

# Build the site
hugo --gc --minify

# Check for templating gone wrong
echo 'Scanning for bad template escaping...'
if grep --fixed-strings --recursive --ignore-case zgotmplz public ; then
    echo 'Found zgotmplz, something is wrong with your templating'
    exit 1
fi
echo '...Templates check out.'

# Drop hi-res original images
themes/capnfabs-lite/buildscripts/drop-resources.py public

Check it out

You’re checking out the theme here on this website!

But you can also check out the code at github.com/capnfabs/hugo-theme-lite. Feel free to create an issue on Github or contact me if you’ve got any feedback or questions.


  1. Last time I overhauled this blog, I wrote about how much had changed in the way people consider identity on the internet. [return]
  2. Well, at least it was my build script until I wrote this post, which violates the assumption that ZgotmplZ would never be in content I’d authored intentionally 😅 Now, I’m explicitly excluding this one specific file from the check. [return]