Introduction
This blog is created with Hugo and I had an absolute blast so far using it. In doing so, I have learned a few things I want to share. This is basically a personal tips & tricks list and if you have stumbled upon it I hope you find it useful!1
Undocumented Behavior
While I love Hugo I think that its documentation really could use some work. Some of its functionality is not properly explained, which is an absolute shame since there are some fantastic features built into Hugo which I want to highlight in this section.
Filenames and Fingerprint
Using cache control we can instruct a client to cache some resource of our website for a fixed amount of time. This is helpful for CSS and JS files, since they should not often change. However, what if the resource did change while a client still held it in its cache? The resource will not be updated!
A simple solution is to give the resource a different name when its content changed.
This could be achieved with version numbers, but it is much easier to just use the Fingerprint
function of Hugo. This function can be used, to compute a secure hash of the file which can be accessed through the .Data.Integrity
property, which can be used to implement SRI.
At the time of writing, the documentation for the Fingerprint
function does not mention, that the filename will also be changed!
Using pipes, we can load a resource and apply the fingerprint to it to generate a new resource:
{{ $cssResource := resources.Get "css/foobar.css" | resources.Fingerprint }}
The name of the resource now bares the files hash!
Using the .RelPermalink
property we will see a file name like /css/foobar.<hash value>.css
.2
Therefore, the filename automatically changes when the content of the resource changes.
This allows us to use a very high max-age
directive for our servers cache control, without worrying that clients will not update a resource.
Similarly to Fingerprint
, the Minify
function also adds a .min.
to a file’s name.
As of the time of writing, this is also not documented.
Of course, when both Fingerprint
and Minify
are applied to a file, both changes will be present in its filename.
When to Fingerprint?
When using both Fingerprint
and Minify
which should come first?
The order does not make a difference in the file’s contents as Fingerprint
does not change them, only Minify
does.
What does change, however, are the contents the fingerprint is calculated on.
Fingerprinting before minifying allows you to change the fingerprint by adding contents to your file which are later removed (such as comments).
The fingerprint will change if the actual source has changed.
However, this also means that the fingerprint cannot be used for SRI, since it does not fit with the actual file contents.
Combining Sass, SCSS and CSS
Hugo has a Sass to CSS transpiler built in which can be used with the ToCSS
function.
Using Concat
we are able to bundle Sass and CSS.
The only important thing to look out for is that resources that should be bundled have to have the same type.
For our purpose it is easiest, to transpile everything to CSS and then combine the files.
Here is an example how I use these functions to combine normalize.css with my site’s primary SCSS file:
{{ $style := resources.Get "css/style.scss" | resources.ToCSS }}
{{ $normalize := resources.Get "css/normalize.css" }}
{{ $bundle := slice $normalize $style | resources.Concat "css/bundle.css" | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $bundle.RelPermalink }}" integrity="{{ $bundle.Data.Integrity }}"/>
In order to combine resources the slice
function can be used at the start of the pipe.
Using Fingerprint
the filename will change if any of the bundled resources changes the minified output.
A Shortcode for Resizable GoAT Diagrams
Hugo’s built-in support for GoAT diagrams is a wonderful feature to me. The diagrams are easy to make, look good and are embedded as SVG in the resulting HTML document, so loading and rendering them isn’t an issue.
The only problem I have with them is how they are included in the source.
We essentially define a listing with the goat
language.
Hugo then automatically renders the diagram.
I don’t like that because it doesn’t give me the chance to put the diagram into some container I could use to restrict the size of the diagram. Sadly, there is no shortcode for GoAT diagrams that would let us create another shortcode from it. So, let’s build a solution and create one!
In order to render the inner content of a shortcode as a GoAT diagram we have to get creative.
Inside of the shortcode we can use .Inner
to get the content from the source file, but how do we transform it to a diagram?
Using printf
we can put the content into a goat
listing and using markdownify
we tell Hugo to treat the result as normal input for its process to convert Markdown to the finished document.
Therefore, diagram rendering happens in this step.
The code for it looks like this:
<div class="goat-scale" {{ with .Get 0 }}style="width: {{ . }}%;"{{ end }}>
{{ .Inner | printf "```goat%s```" | markdownify }}
</div>
My shortcode lets me set a width for the diagrams container (using a class called goat-scale
). In order to make this container responsive, I use media queries in my CSS to force the container to 100% width if the screen becomes to small.
.goat {
width: 100%;
margin-left: auto;
margin-right: auto;
}
.goat-scale {
margin-left: auto;
margin-right: auto;
}
.goat > svg {
font-family: monospace;
font-size: medium;
}
@media only screen and (max-width: 650px) {
.goat-scale {
width: 100% !important;
}
}
Now, I can create my GoAT diagrams like this and have them automatically scaled:
{{< goat 75 >}}
.---------------. .---------------.
| Web-Server | | My Machine |
| ---------- | | ---------- |
| .---------. | | .---------. |
| | public | <----------| public | |
| '---------' | | '---------' |
| | | ^ |
| .---------. | | .----+----. |
| | dev | <----------| dev | |
| '---------' | | '---------' |
'---------------' '---------------'
{{< /goat >}}
Medium-Zoom + Hugo
I use a JS library called medium-zoom to make images interactively zoomable on this site. Thumbnails are created using a simple shortcode and Hugo’s built in image processing. It looks a bit like this:
This is achieved by this shortcode:
{{< image src="ducks.jpg" >}}
Computing Thumbnails
Let’s first look at the finished image
shortcode and then go more into how it works.
{{ $image := .Page.Resources.GetMatch (printf "*%s*" .Params.src) }}
{{ $width := .Params.width | default "500" }}
{{ with $image }}
{{ $small := $image.Resize (printf "%sx jpg q75" $width) }}
<img
loading="lazy"
{{ with .Params.alt }}
alt='{{ .Params.alt }}'
{{ end }}
src='{{ $small.RelPermalink }}'
data-zoomable
data-zoom-src='{{ $image.RelPermalink }}'
/>
{{ end }}
At first, the resource is loaded using a generous glob search, so that the filename does not have to contain the whole path.
The shortcode has a width
parameter, which specifies the targeted width of the thumbnail image with a default value of 500 pixels.
Using .Resize
and printf
the image is being converted to a jpg with 75% quality to reduce the thumbnails size.
The finished img
tag is being constructed with an optional alt
text, using the thumbnail as its source and the original image in the data-zoom-src
attribute, which is used by medium-zoom.
Images which should be treated by medium-zoom have the data-zoomable
attribute.
All images are lazy loaded by default to save bandwidth and improve the site’s performance.
The full images are only loaded once they being interacted with.
Only serving JavaScript when needed
For this shortcode to work the JS library has to be loaded and the mediumZoom
function has to be applied to the corresponding img
elements.
This can easily be achieved with two lines in the website’s source:
{{ $mediumZoom := resources.Get "js/medium-zoom.js" | resources.Minify | resources.Fingerprint }}
<script defer src="{{ $mediumZoom.RelPermalink }}" integrity="{{ $mediumZoom.Data.Integrity }}" onload="mediumZoom('[data-zoomable]')"></script>
Using onload="mediumZoom('[data-zoomable]')"
will ensure that all tags with the data-zoomable
attribute will be considered.
However, what if a blog post doesn’t have any images?
The library surely shouldn’t be loaded then, but how could we check if any images are present?
We can achieve this with the following function:
{{ with .Resources.ByType "image" }}
...
{{ end }}
When we wrap our script
tag inside this function, it is only included in the document when image files are part of the posts resources!
So, the full inclusion of medium-zoom looks like this:
{{ with .Resources.ByType "image" }}
{{ $mediumZoom := resources.Get "js/medium-zoom.js" | resources.Minify | resources.Fingerprint }}
<script defer src="{{ $mediumZoom.RelPermalink }}" integrity="{{ $mediumZoom.Data.Integrity }}" onload="mediumZoom('[data-zoomable]')"></script>
{{ end }}
Technical Stuff
These are some tips & tricks that do not directly have something to do with Hugo, but are still related to it.
Lazy Load Everything
When tuning and measuring website performance using Lighthouse it becomes clear quite quickly that loading times of resources are a huge problem. When loading a site browsers need to download images, fonts, CSS and JS files, process all of them and then render the content to the screen. This means network latency affects the time it takes to render the website drastically.
A solution to this problem is lazy loading, meaning that a resource is only loaded once it is needed or when there is time for the browser to do so. This is ideal for non-criticial resources, such as optional CSS, fonts or JS scripts that don’t provide necessary functionality.
While I could write a lot about how to do that I will just link to some much better resources that I found useful:
- JS:
defer
to avoid waiting for the script - CSS:
rel=preload
to avoid render-blocking - Fonts:
font-display: swap;
to not delay text rendering - Images:
loading="lazy"
to defer loading until in range
Deploying a Website using Git
Hugo and other static site generators make it simple to deploy the website using version control systems. Here is how I do it: I use Git in conjunction with Plesk to deploy and manage my website.3 There exist two repositories on my server and local machine, one for the development of the blog, containing the source code of the site, and one for the public facing site, containing the compiled files.
On my local machine, I can develop and generate the public facing site and still have independent version control between actual source code and deployed static resources.
This process is rather lightweight, fast and requires minimal interaction with my server except for a simple git push
.
-
If I stumble over any other useful stuff in the future I will add it to this post. ↩︎
-
It seems that absolute URLs did not work correctly with fingerprinting at some point. While not being able to reproduce this issue I will use relative paths regardless. ↩︎
-
Setting up Plesk with Git is rather simple. If you want to use a similar setup you can read a tutorial on how to do that here. ↩︎