Optimize your Hugo Blog With Unlighthouse
Recently Google let me see the new Analytics dashboard (they moved from… Something something analytics to something something Analytics 4). And one of they things I saw was that my blog’s performance was bad!
In general the blog’s infrastructure didn’t get much love since I opened it a few years ago, so a cleanup for performance and content is probably in order.
Get initial Lighthouse results
What’s Lighthouse? It’s a tool that Google provides to measure the performance of your site. It’s available as a Chrome extension, but also as a CLI tool that you can run on your site. So I thought that the first order of business was to run Lighthouse on my site and see what I’m getting both for Mobile and Desktop.
Then, thanks to a Fireship video, I found out about Unlighthouse, which seems even better for this use case! Unlighthouse runs Lighthouse reports for all pages of your site, and then generates a report with the results.
So let’s run it! From the
getting started guide, looks
like we need to use Node.js 16. To make this “clean”, add a .nvmrc
file to a
new folder in your Hugo site called performance
.
nvm use 16
npx unlighthouse --site https://mrnice.dev/
The report is so cool:
Analyzing the results
This is a lot of information. Let’s start looking at it.
UX is more important than the machine
Looking at the homepage itself as it loads, there are too many words. I’ll slim it down - even if the site loads fast, if the homepage is confusing, people will bounce out. So let’s slim it down.
Before
After
Much better. Let’s go the analytics themselves now.
Wait, which device and I looking at?
I know that most people are using my site from Desktop AND Mobile.
We need to look at both. Looking at the report, looks like it’s from “emulated
mobile”, so we need to re-run it for Desktop. A quick look at the documentation
for the configuration and the
“selecting device” section shows
that we’ll need to set up a configuration file for each option. Since I know
that eventually I’ll want to automate this, it’s a good chance to write a shell
script to work with this. I went a little overboard as I tern to, and used gum
to build a fun CLI tool for this, which I’m dubbing lighthouse-plus.sh
.
#!/bin/zsh
# Check that gum is installed
if ! command -v gum &> /dev/null
then
echo "gum could not be found, run 'brew install gum'"
exit
fi
# Check that currently using node 16
if [[ $(node --version) != *"v16"* ]]; then
echo "Please use node 16"
exit
fi
# Get device name ('desktop' or 'mobile') from command line argument, if not
# provided, use `gum choose` to choose interactively
if [[ -z $1 ]]; then
device=$(gum choose "desktop" "mobile")
else
device=$1
fi
npx unlighthouse --config-file "$device"-unlighthouse.config.ts
Now we can run lighthouse-plus.sh
and get the results for both Desktop and
Mobile. Let’s iterate on Desktop and the Home page first: Many fixes will be
relevant to all pages, and it’s the most important page and device. Also, it’s
what I use right now for development so iterations will be faster for me 🤸♀️.
Improving performance
Reduce unused JavaScript
The first thing that jumps out is that I have two unused JavaScript files:
The first one, https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
, is
from “How to Add Math Expressions to Hugo Blog - the Shortest Guide Possible”,
which I honestly forgot about. I don’t have any math expressions in my blog’s
homepage, so why am I loading it here? Let’s look at the old post…
Choose where to include JS scripts
To display math we’re going to use a JS script. We’ll include the JS in our
layouts/_default/baseof.html
file, to make sure that we have Math support in every single page.
Oh. That’s why. I put it there.
To fix that, I tried using page parameters, and only include the script on pages
that need it. To debug the templates and figure out which .html
file I want to
edit (instead of baseof.html
), I used a cool trick: I added a {{ printf "%#v" . }}
to the top of the file, and then looked at the output of the page. This
gave me a bit of useless info:
& hugolib.pageState {
pageOutputs: [] * hugolib.pageOutput {
( * hugolib.pageOutput)(0x14000c4cc80), ( * hugolib.pageOutput)(0x14000c4d040)
},
pageOutputTemplateVariationsState: ( * atomic.Uint32)(0x14000238180),
pageOutput: ( * hugolib.pageOutput)(0x14000c4cc80),
pageCommon: ( * hugolib.pageCommon)(0x14001177200)
}
I went through a bunch of the html
templates in the layouts
folder, until I
found the right one (for my template, it’s themes/hermit-fork/layouts/posts/single.html
,
but of course for your site it’s going to probably be different).
Then, when I replaced the previous printf
with {{ printf "%#v" .Params }}
, I
saw exactly what I needed:
maps.Params {
"date": time.Date(2021, time.June, 12, 12, 15, 16, 0, time.Local),
"draft": false,
"images": [] string {
"/images/math-teacher.png"
},
"iscjklanguage": false,
"lastmod": time.Date(2021, time.June, 12, 12, 15, 16, 0, time.Local),
"publishdate": time.Date(2021, time.June, 12, 12, 15, 16, 0, time.Local),
"render_math": true,
"tags": [] string {
"howto", "hugo", "meta", "math"
},
"title": "How to Add Math Expressions to Hugo Blog - the Shortest Guide Possible",
"toc": false
}
See the render_math
there? It’s because I added this parameter to the post’s
front matter:
---
title: "How to Add Math Expressions to Hugo Blog - the Shortest Guide Possible"
date: 2021-06-12T12:15:16+03:00
draft: false
toc: false
images:
- "/images/math-teacher.png"
tags:
- howto
- hugo
- meta
- math
render_math: true
---
I could even live-debug my if
statement with {{ if .Params.render_math }}
which returned true
! The final working solution was to add this to the
template:
+ <!-- MathJax start -->
+ {{ if .Params.render_math -}}
+ <script type="text/javascript" id="MathJax-script" async
+ src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
+ </script>
+ <script type="text/x-mathjax-config">
+ MathJax.Hub.Config({
+ tex2jax: {
+ inlineMath: [['\\(','\\)']],
+ displayMath: [['$$','$$'], ['\[','\]']],
+ processEscapes: true,
+ processEnvironments: true,
+ skipTags: ['script', 'noscript', 'style', 'textarea', 'pre'],
+ TeX: { equationNumbers: { autoNumber: "AMS" },
+ extensions: ["AMSmath.js", "AMSsymbols.js"] }
+ }
+ });
+ </script>
+ <script type="text/x-mathjax-config">
+ MathJax.Hub.Queue(function() {
+ // Fix <code> tags after MathJax finishes running. This is a
+ // hack to overcome a shortcoming of Markdown. Discussion at
+ // https://github.com/mojombo/jekyll/issues/199
+ var all = MathJax.Hub.getAllJax(), i;
+ for(i = 0; i < all.length; i += 1) {
+ all[i].SourceElement().parentNode.className += ' has-jax';
+ }
+ });
+ </script>
+ {{- end -}}
+ <!-- MathJax end -->
Before:
And after:
Removing JS from the critical path with Partytown 🎉
Now the GA-4 GTM script is slowing down the page even more! Damn it!
I don’t want to remove it, but I also don’t want it to slow down the page. So I opted to install Partytown, simply following this guide.
There were a few things that didn’t work 1-to-1 from the guide:
- I had to replace the internal analytics partial with a custom one that uses
type="text/partytown"
instead of normaltext/javascript
. So I copied the original partial from Hugo’s files on GitHub. - Since I deploy to GitHub Pages, I had to cause Jekyll to add the
~partytown
directory. Since Jekyll doesn’t include directories that start with certain special characters when building and deploying the site. - I used
nvm
and an.nvmrc
file to make sure I’m using the correct Node.js version.
To fix 2, I added the following _config.yml
file to the root of my site (the
public
directory):
include:
- "~partytown"
Improving CLS (Cumulative Layout Shift)
This one was simple: Just slapped height
and width
on the homepage logo. I
found out that I’m loading that image from a 3rd party site instead of from my
static files, so updated that as well.
Improving accessibility
I only got 74?! That sucks, I really want the site to be accessible. There were a bunch of issues here:
- No alt text on the site logo. Added descriptive text
- “Background and foreground colors do not have a sufficient contrast ratio”.
In the Hermit theme, in
_predefined.scss
, there’s a@mixin
calleddimmed
, which lowers the opacity by 40%. While it looks cool, the contrast IS hard to read. So I changed it to 90% instead of 60%, and now it’s much more legible, even if it’s not as cool.
Improving SEO
“Document does not have a meta description”
After Googling for a bit, this seems like a known issue that hasn’t been fixed
yet, see Pull Request. I just copy
pasted the fix from the Pull Request (including the CR comment) by adding the
description to the structured-data.html
partial and the description to my
config.toml
file.
In config.toml
:
[params]
description = "..."
In themes/hermit-fork/layouts/partials/structured-data.html
:
<meta
name="description"
content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end -}}"
/>
“Structured data is valid”
I didn’t know about this one! Appears that Google has something called “Structured data” which is a standardized format for providing information about a page and classifying the page content. Seems like we want two features here:
- Add the site logo as the Structured Data logo using this guide
- For each blog post, add the “BlogPosting” structured data using this guide
I started with 1 for the homepage, by adding this into the structured-data.html
partial:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"url": {{ .Site.BaseURL }},
"logo": {{ (print .Site.BaseURL .Site.Params.siteLogoRelPath) }}
}
</script>
And this to the config.toml
:
[params]
siteLogoRelPath = "images/site-logo.png"
Then, run your site locally, inspect the <head>
until you find the JSON-LD
tag, copy it, and paste it into the JSON-LD Playground to test that it worked out correctly.
Re-run and compare
I WIN!
Other random cleanups
Get rid of the “tweet” Hugo shortcode warning
Every time I ran the server I saw these warnings: The "tweet" shortcode will soon require two named parameters: user and id.
. I decided to fix it instead
of continuing to ignore the warnings (I’m not a monster). It’s a simple change:
-{ { < tweet 1388749844240535553 > } }
+{ { < tweet user="ShayNehmad" id="1388749844240535553" > } }
Summary
I learned a TON about Hugo, Jekyll, GitHub, and the Web in general. I am also DONE, with only two side-projects left for later: Automate this with CI, and add structured data to the blog posts. Another day!
Photo generated by DALL-E with the prompt “An expressive oil painting of a lighthouse watching over a sea made from paper”.