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:
- A mechanism for excluding source images after they’ve been processed or resized,
- Visual differentiation for drafts, and
- A way of preventing Go templating errors from sneaking their way into your site output.
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:
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.After building your site for deploy (i.e. by running
hugo
), run:themes/paperesque/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:
posts/example/index.html
andposts/example/index.droplist
.
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:
- Recursively finds all
.droplist
files in the Hugo output directory - Loads each droplist file, to collect a complete list of all original-sized resources that were marked for deletion
- Deletes all the marked resources specified by the droplist files, and finally
- Deletes all the
.droplist
files.
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:
… and on single post pages:
… and even 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:
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/paperesque/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/paperesque. Feel free to create an issue on Github or contact me if you’ve got any feedback or questions.
Last time I overhauled this blog, I wrote about how much had changed in the way people consider identity on the internet. ↩︎
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. ↩︎