Experiences migrating to Hugo

  • en

by on 2023-02-12 | License Permalink

I started generating this site via a static site generator in 2021. Initially it was meant to be a short-term solution because my dynamic Indieweb software powering my site had fallen over and wasn’t generating forms properly, meaning I couldn’t post. I instantly fell in love, however, with the workflow of generating a static site and I will need some serious convincing to go back to a dynamic site.

The site generator I chose was Jekyll because it was what I knew from using it literally once or twice during my PhD for some project websites. After some wrestling with trying to get a site working without a theme (so I could use my own CSS and templates!) I fell into a comfy rhythm of updating it and adding features such as my long-desired ability to write posts and pages in multiple languages, courtesy of Anthony Granger’s blog on the subject.

Then some of the blogs I follow recommended Hugo. They touted speed as well as a few design features that I wrote off as not needing; my site is quite simple with only a few layers and very simple templating requirements. At the same time, however, I began wanting to drastically minimise the software on my system and automate my setup on new machines via dotfiles.Jekyll requires that I install Ruby — basically an entire programming suite complete with its own package manager — and then install Jekyll via the Ruby gems system. Not very minimal, considering that I only use Ruby for Jekyll.

In addition to this, during my exploration I discovered Hugo’s multilingual mode which looked promising. This meant that Hugo covered basically both of my requirements:

  • A cleaner install process / fewer dependencies. Getting my blog up and running on a new machine required setting up Ruby, then Jekyll. Sometimes there were dependency issues. Hugo is a single compiled binary available in most package managers and thus installable via apt.
  • Easier multi-lingual support. Despite the fact I was happy with my multilingual solution for Jekyll, it still required a fair bit of manual wiring and adding a new language would mean a lot of manual insertion of logic into templates.

The end result is that I’ve migrated this site to use Hugo rather than Jekyll. This post continues by reflecting on my experiences. There are plenty of blogs and tutorials which discuss migrating a site from Jekyll to Hugo. That’s not what I’m going to consider here. This is a reflective piece on how I found the experience and what I took away from it. I cover some technical details of Hugo and Jekyll as a by-product of the reflection, but this is not a tutorial or in-depth discussion of migrating particular features.

The migration experience

I began considering migrating this site sometime in 2022, but had a few false starts with Hugo. Despite Hugo’s documentation being fairly comprehensive, I found it very difficult to wrap my head around how to build the site that would work for my needs. This was basically due to how themes are presented in the Hugo documentation and quick-start guide.

Essentially, Hugo’s documentation is adamant that you need to install or build your own theme. It’s very easy to follow the instructions to set up a Hugo site when all you want to do is install a pre-built theme and write some content, or extend/build a complex theme of your own which is great for most people.

I found, however, that this made it very difficult to approach building a site in Hugo from a position of “I just want to apply my HTML templates to some content and hang some CSS off of it”. The resources seemed to be pushing me to build my own theme from scratch which can be a little daunting for me learning a new tool-chain and I got a little bit overwhelmed when Hugo was discussing index templates vs list templates vs single templates and then discussing look-up hierarchies all at once.

Like all confusing things, however, once I had approached it and digested pieces of the learning curve piecemeal – it all clicked into place gradually and my momentum built nicely. The key moment for me was figuring out that I didn’t actually need to develop my own theme to build a site and could just create the static and layouts folders to achieve what I needed.

I’m pretty happy now that — unless something major comes along in the next few years or Hugo disappears from the Debian/Devuan repos — I’ll be sticking with Hugo for some time for my personal site and will likely be using it to build other sites in the future when a static site is appropriate.

Things I adore from Hugo

The first thing I liked when starting up a Hugo site was the folder structure; it’s so nice and tidy. One of my grumbles with Jekyll was that, while it is “blog aware”, page content lives at the same level as configuration files. Post live in the _posts folder but it felt a little messy to have markdown files representing pages at the same level as my site configuration files and other types of static content such as CSS. Layouts lived in _layouts which was nice, but the _includes directory was a sibling which meant that the root folder had two different locations to deal with template files!

In Hugo, all of my content lives in the content folder, static files live in static, and all of my layouts and templates live under layouts in an appropriate directory structure. I also love that the i18n folder exists for use with multi-lingual content. This leaves a single configuration file at root. Perfect!

Related to this is the typologies or sections feature. I really like that I can just create different folders under content and this is reflected not only in the URL structure of the generated site, but the different types of content/sections can use different templates when required. I can foresee see this being very useful in the future if I ever want to develop a static site with a more complex structure than my personal site.

As a direct result of the sections/typologies feature, I’ve found that there’s less ongoing manual wiring with Hugo as compared to Jekyll. The initial setup is indeed tougher and I actually struggled with the concept of ‘list’ and ‘single’ pages initially because of Hugo’s assumptions that you probably want to generate these type of pages for each type of content. This isn’t to say that the assumption is wrong, but I had to manually disable this to get the effects I wanted. Once this initial setup was done, though, there is far less redundancy in my Hugo content files. In Jekyll I had to specify which layout each page or post was using. In Hugo, this is assumed automatically from the section name and looking up the corresponding single template. Bliss. I’m given to understand that the option to manually specify a layout on a piece of content is there, though, for edge cases.

This lack of manual wiring for everything carried through when I enabled multilingual mode. After some initial setup to ensure that I could organise my folder structure to my preferences, I found Hugo’s multilingual mode to be intuitive and fun. There are multiple ways to organise content in multilingual mode and my preferred way was a quick configuration line away. This means that I have a folder per language under my content directory, with translated content having a shared filename and language-specific content only requiring that I name it something unique.

The convenience didn’t stop there, though. Whereas in Jekyll I had to run queries in my templates to pull out language-specific content based on the current page language – with Hugo it automatically applied the language as a sort of context and it only pulled out the menu items or list of posts for the current language without any additional logic in templates. Translating strings was a breeze using the i18n folder, intuitive template directives, and a simple YAML file per language. When it came to generating RSS/Atom/JSON feeds for each language it turned out that there was a simple command to retrieve the relative link for these files based on the current language context. Sheer brilliance.

There is only one grumble I have with Hugo’s multilingual mode. I want much more fine-grained control over how the URL paths for various content types are rendered under multi-lingual mode. Hugo automatically produces a mirror of my URL paths underneath a language code for every site language that isn’t the default. For me this means that everything in Esperanto lives under /eo/. This is fine and actually the behaviour I want for pages and feeds. For posts, though, I sort of want them all to live under the same /YYYY/MM/DD/{slug} path regardless of language. This is related to how I view post content as its own thing, and how I have two separate streams of posts for each language which may or not be translated back and forth.

This said, I think it’s a fair trade-off. Hugo’s multilingual mode covers literally all of my other needs and setting up translations for content requires much less work than achieving the similar effect in Jekyll. I was previously needing to embed logic in templates to check for specific languages, whereas now I just include a single template directive to reference a piece of content and then Hugo’s folder structure takes care of the rest. Much nicer.

Menus was another major convenience. In Jekyll, Menus are not implemented as a specific feature and it’s left up to the template designer. This is, in some ways, fine. It requires less wiring at the content level. In Hugo, I was able to add specific pieces of content to various menus (and I had a choice as to how to do it!) and with specific weights. I could then directly reference my menus in my templates in order to generate them on pages quickly without needing to do any querying or sorting at the template level. This was a very nice touch.

Another quality of life feature was the Partials and Shortcodes features. For me, these weren’t as mind-blowing as I’d heard some bloggers/vloggers make out, but perhaps that’s because I already have a history of building reusable snippets of templates from my days using Twig in PHP/Symfony and from Liquid when I built the site in Jekyll. For me, the quality of life feature came from the fact that Partials and Shortcodes have distinct and separate use-cases.

Partials are more like the classic include directives from other templating systems. You write some reusable HTML/XML/etc and then can use these within your templates; this keeps your template logic relatively clean and modular as well as making your Partials reusable. Shortcodes operate in basically the same way, but are explicitly designed to be included in your content files. This is a nice separation of concerns and avoids mixing up templates related to the page-templates as opposed to structures which are used to render content. Making this even more useful is the fact that — when I’m including a shortcode — I can specify whether the resulting content is pre-rendered HTML or is markdown content that requires processing. The result is that I can include my blogroll as some simple markdown and include it on every translation of the Links page without repetition, and also create a shortcode to generate a HTML table of contents for posts which I can include whenever I need it rather than having complex logic at the template level.

Things I miss from Jekyll

Despite the fact that I’ve found Hugo caters to all my needs and works in a way that my brain prefers, I do still miss some of the elements of Jekyll.

The main thing I miss is the way that drafts work. To my knowledge, the draft status of a piece of Hugo content is assigned in the content file. That is, I can include draft: true in the header to prevent a piece of content being rendered accidentally. In Jekyll, there was a dedicated _drafts folder which wouldn’t render unless a specific directive was given to the built-in server. I prefer this for the simple reason that I often have multiple drafts going on at once and it’s much easier to have them live in a single folder rather than having to manage the draft status in content files. There may be a way to change this behaviour or duplicate the old Jekyll-style, but I haven’t looked into it. As it is, I find it a little more difficult to keep track of drafts at the moment but I imagine I’ll get used to it.

The other thing I miss is the templating language, or specifically the control logic built into it. As noted earlier, Jekyll uses the Liquid templating language which was somewhat familiar to me coming from a PHP/Symfony/Twig templating background. I am all for migrating and learning templating languages but, to me, I found the Go Templating language to be a little unintuitive at times when I needed to e.g. loop over the results of something. I also found it a bit odd that I couldn’t access the page variables from inside a loop; everything was set at the loop scope with no way to reference the previous or surrounding content context. This meant that I needed to assign a variable outside the loop to enable my language menu. Maybe that’s a technically better way of doing things, or requires less memory, but I found it a bit different and jarring at first. I imagine I’ll get used to this though.


I migrated the back-end of this site from Jekyll to Hugo, learned a lot from the process and found I broadly prefer the Hugo way of doing things. My site is so simple, and Hugo is so fast, that it generates before I can blink but the main benefit is in quality of life in organising content and maintaining the site templates. I never have to repeat myself, everything is clean, and all of my needs are catered for by a single binary I can install from a single apt command without adding an entire tool-chain of bloat to my system.

This has ultimately made this site more portable, easier to maintain, and more pleasurable to write while allowing me to tinker and learn some new paradigms and skills. I look forward to many years with Hugo.