Dmitrii Aleksandrov

Fearless Website Updates With Hugo

Posted on 8 mins

Tech

Managing dependencies, reviewing generated HTML, writing a script for diffing staged changes. Sounds fun?

SSGs are complex and fragile

I use Hugo to make this website. Hugo is awesome. It has allowed me to download a theme and start writing in Markdown, instead of bothering with templates and styling 1. It generates RSS feeds and other metadata like sitemap.xml, helps with content management , provides a development server with hot reloading, and does many other useful things.

But when you use a full-blown static site generator like Hugo (and especially when you rely on a third-party theme!), you depend on a moving system that you don’t fully understand. Small config changes and routine software updates can break your website in subtle and unexpected ways.

Dependency updates are fragile

Apart from potential bugs, new Hugo releases regularly include intentional breaking changes. For this reason, I always build the website with a specific, tested version of Hugo. I update Hugo manually when I’m ready to re-test the website and adapt to the changes. I recommend this to anyone using Hugo. Having full control over the pace of change is one of the best features of static site generators. 2

But this only allows you to control when the changes happen. Usually, you still want to upgrade eventually. So, you still need a way to test the result.

The most obvious way of doing that is just browsing the updated website locally. Messing around.

That’s what I did when I tried upgrading from Hugo 0.131.0 to Hugo 0.132.0. Everything looked good until I clicked on the RSS link. If I hadn’t done that, I wouldn’t have noticed that Hugo 0.132.0 breaks my RSS feed! 3

That’s not something that you can notice when browsing the website normally. I just got lucky.

Any edits are fragile, really

If you just want to blog 4, you’re probably ready to say: “fuck it then”, stop fiddling with styles, and stop updating Hugo. Basically, to stop making any unnecessary global changes to your blog.

That could help, indeed. But here’s the problem. Due to the “G” in “Static Site Generators”, seemingly “local” changes can mess up other parts of your site too!

Changes in specific configs, templates, even in your content. In the end, that RSS bug was triggered by a specific post with a " character in the description. I could live in an alternative reality where I updated Hugo first, and then published that post. I wouldn’t expect such destructive results from publishing a post!

Another example. While working on that same post (the 6th on my website), I noticed that the list of posts shows only 5 posts per page by default, and ugly premature pagination kicks in after that. Luckily, I have a habit of checking that page to see how the post summaries look in context. So, I caught that issue in time.

I’ve always felt a little paranoid knowing that my manual testing can’t cover everything. I don’t open every page and don’t pay full attention to every line. Instances like this made me more and more paranoid over time.

My inner software developer really wanted to get out, overengineer this blog, and automate everything. And I’m finally letting that happen.

Automation comes in

The basic issue is that I can’t check every page on every update. I want to know when pages change. I don’t want to manually check my RSS feed every time I update Hugo, update the theme, change a config, edit a template, or edit my content. I want to see a list of changed pages, with a specific diff of the changes. And then check only these changed pages in the browser, if needed.

Writing a script

The basic idea is to simply diff the old and the new version of the generated files.

But where do I get the old version from? If I just use whatever is lying around since the last run, and I’ve switched to a different branch since, then I’m going to see the extra diff between the branches! I need the to-be-committed diff specifically. To get that, I’m going to generate both versions from scratch: the staged version of the website, and the “clean” state of the current branch before these changes.

With the help of git stash, it’s very easy to temporarily “clean” the state. We clean everything, generate the “old” version, restore the staged changes, generate the staged version, and then restore the full unstaged local state that I had.

In the end, after I run the script, the local state is unchanged. The result looks as if the script has never touched Git in the first place! The only observable effects are the generated output folders.

The whole thing takes around 0.3 seconds on my laptop. Tools like Git and Hugo are great not just because they are fast, but because they are composable and can be used to build other fast tools on top.

Now the only thing that’s left is actually diffing the two output folders. I don’t like old-school diff output. I prefer an easier-to-navigate GUI with a separate split view for every file, like in VSCode. I know that I can launch code --diff old.file new.file to open a diff view comparing two files. It’s very convenient, but it can’t compare folders. So, I use an extension for that. I haven’t yet found a way to trigger the extension from the script. But opening it manually in VSCode is fast enough, too.

Here’s a permalink to the current version of the script , in case I move it later. To see the latest version, explore the repository starting from the README. I keep my workflow documented.

The results

After writing this script, I finally got myself to leap over several Hugo versions in a few days , fixing all the issues that were preventing the update. Detecting and debugging these issues was much easier with the diff.

Now I check the diff on any major change, such as updating Hugo, updating the theme, publishing new posts, editing configs or templates. I feel much more confident and encouraged to make these changes. Even semi-automated testing can do wonders for your velocity and mood.

I love Rust’s “fearless concurrency” approach and using my PC “fearlessly” in general. I’m inspired by this idea. It has become one of my main guiding principles.

Outstanding problems / future ideas

Making a website is hard . As fearless as I’d like to be, I still don’t have the time and energy to find, choose, and stitch together these tools. But it’s still very nice that SSGs even provide this “preview” output that can be diffed and validated, and doesn’t auto-update under my feet.

Eventually, I want to see Hugo website validation become a solved problem with a recommended tool selection, CI workflow examples , and so on. I’ll post an update if I make any progress on that.


My favorite posts about the tradeoffs of SSGs in general:

Rust’s “fearless” development philosophy:

Discuss


  1. At least, initially! I’ve edited some styles and templates since. ↩︎

  2. Statically-linked generators like Hugo take this to the next level.

    You can just stay on an old version. All Hugo releases from 12 years ago are still available as binaries that you can download and run. These old Hugo binaries will keep working forever, unless your OS changes in a radical way. They don’t stop working if you update Python. They don’t stop working if you update Node. The installation doesn’t fail because it couldn’t download some library. No bullshit like that.

    There’s little to no security risk in staying on an old version of a static site generator. Being up-to-date matters when you have an online CMS (such as Wordpress ) that’s exposed to the Internet. ↩︎

  3. Actually, Hugo just exposed a bug in my theme’s template. But, as always, I can rarely tell right away whether it’s Hugo or the theme that’s responsible for an issue that I’m having. ↩︎

  4. And if, like me, you still want to use a static site generator instead of a cloud platform. ↩︎

Comments