I recently went through the process of switching my blog’s static site generator from Jekyll to Hugo. Both are great tools, but as I’ve updated my website over time, I began to feel constrained by Jekyll — and wanted to switch to something more stable and robust.
I really like Jekyll. It’s fast to setup, comes with an extremely expressive templating language (sometimes dangerously so1), and has the full support of Github — all of which lowers the barrier to deploying and maintaining a personal site.
That being said, Jekyll’s generation times start to skyrocket when you develop a site with meaningful complexity. I frequently had site generation times of 7+ seconds, which made working on formatting fixes on my website a complete slog.
Additionally, I’m not the biggest fan of the Ruby ecosystem — mostly due to the trouble I seem to have when installing dependencies. I’ve learned to accept Python’s dependency management warts — they’re generally ameliorated by sticking with a single tool and using it as intended. Ruby… I’ve never gotten to behave well. Without fail, I run into some byzantine installation bug with Nokogiri or Bundler when setting up a completely clean environment. I inevitably fall down some rabbit-hole of desperate StackOverflow questions, costing me an afternoon of annoyance.
Despite this, I stuck with Jekyll for several years — I had a large switching cost in moving away from it: it’s the SSG that I have the most experience with and have used with my site since I started college. However, I reached a tipping point.
After using Netlify for a bit, the “moat” that Github Pages’ blessed support afforded Jekyll fell away. It’s just as easy to deploy a Hugo-, Gatsby-, or Pelican-generated site on Netlify as it is a Jekyll-generated one (and with HTTPS, no less!).
In fact, the experience of deploying using Netlify is (in my opinion) superior to Github Pages because you can actually see deploy logs and build failure messages instead of getting a generic email from Github saying “Something went wrong”.
Just as much as I wanted to switch away from Jekyll, I was intrigued in switching to Hugo.
Hugo is Fast My biggest complaint with Jekyll was the page generation times. Hugo boasts that it can have sub-millisecond page generation times — I’ve found that to be a slightly over-optimistic. However, my site’s total generation time dropped from 7+ seconds to ~300ms, so I’d score that as a win.
Hugo is written in Go This is a pretty superficial reason to change SSGs, but… I like Go. It’s a verbose language, but that makes projects written in it easy to read. Its templating system is really good, and Hugo makes smart additions to get similar expressiveness to Liquid (Jekyll’s templating system) with notable performance improvements.
Hugo is a Single Binary The installation and dependency management process for maintaining a Hugo site is trivial compared to Jekyll — you just have to download the single
hugo binary. That’s it. No messing with package installers or system libraries (cough Nokogiri’s love-hate relationship with lxml cough).
Hiccups During Conversion
To get a grasp on Hugo’s conventions, I followed Sara Soueidan’s excellent “Migrating from Jekyll+Github Pages to Hugo+Netlify” guide. There are many Hugo introductions out there, but I found this one to be comprehensive.
Essentially the process for switching from Hugo to Jekyll was this:
- Rewrite my Jekyll page templates in Go template language.
- Restructure my site’s templates and content to fit Hugo’s directory structure.
- Restructure my
config.yamlfile to conform to Hugo’s expectations.
- Add some asset pipeline magic with
gulpto minify assets and compile SASS.
- Fix any miscellaneous errors that I didn’t catch in steps 1-4.
Most of this was fairly self-explanatory — once you understand Hugo’s “philosophy.” However, I still encountered a couple hiccups that are worth mentioning:
Hugo is much more sensitive to directory structure than Jekyll. This was the single most annoying thing about Hugo. Jekyll will render just about anything. Except for a few special directories (i.e.
_includes, etc.) Jekyll renders essentially every file in your site source to the build directory.
Hugo is selective. If a page doesn’t have a valid template or there’s something wrong with a template, the page simply doesn’t get built — often quietly, without error.
You really do have to follow the template lookup orders that Hugo lists in the documentation, else you risk severe frustration. I suggest making default layouts (i.e. in the
_default folder) for every page type so you don’t run into the issue of “quiet non-rendering”.
Hugo is much less flexible in templating than Jekyll. In Jekyll, anything goes. Want to put some Liquid interpolation in your front-matter? Sure why not. Pull in all your SCSS and run a SCSS compiler in a template? Yes, you can.
Hugo is much more conservative. It draws a much clearer distinction between what is a template and what is “content”. This was a bit of an annoyance for some custom pages I have (i.e. my projects page2) that are highly custom — it doesn’t “feel” right to have these pages be templated, but Hugo requires that they be templates anyway.
However, I find that in the long run, I spend much less time writing and editing templates than adding Markdown-formatted content. As long as I could get a comfortable set of Hugo templates that span the distinction between content and template, Hugo is really not any more restrictive.
Hugo is more of a CMS than Jekyll is. Many times in the conversion process, I felt like Hugo was over-engineered for my use case. I don’t have a site with multiple authors/feeds or produce enough content that first-class support for categories and tags is crucial.
Yet Hugo includes all of this and more. It’s a bit overwhelming. Hugo has the marks of a top-down engineered product — “prescriptive to what it thinks users need” — in comparison to Jekyll’s bottom-up approach — “you build only the features you actually use”.
However, by having someone else make all these project layout decisions for you, the result is a fairly clean and consistent project directory.
I still found myself having to turn off many of Hugo’s more advanced features to avoid needless page generations (such as a separate page for each blog post tag).
Hugo’s Plugin Ecosystem is Less Rich Want a SASS compiler or HTML minifier plugin for your SSG? Those exist for Jekyll, but not for Hugo. Fortunately, many have encountered this problem and so the support for Hugo in the
gulp ecosystem is really good. Gulp has top-notch asset pipeline management plugins, so this becomes a non-issue if you’re willing to sprinkle a little Node.js in with your SSG.
My Final Setup
In the end, my asset pipeline looks a lot like Netlify’s victor-hugo template. I use gulp to compile my SASS and minify my assets (HTML, images, etc). The “victor-hugo” template also brings in BrowserSync, which adds automatic refresh on rebuilds, among other niceties.
You may be saying that I’ve effectively traded the Ruby ecosystem for the Golang and Node.js ecosystems — and you’d be right. However, I’m significantly more confident in my ability to fix issues in this environment than I am in Ruby’s ecosystem, so I count that as a win.
I’m hoping that I won’t have to mess with the setup too much. Stability is valuable for a project like this where what I actually care about is writing, and all the SSG scaffolding is a distant second priority.
Ultimately, this can be read as just another “Why I moved from $X to $Y” post3. I’ve given Hugo a spin and I like it so far. It’s fast, expressive where it should be, and conservative where it needs to be.
I try, as a rule, to not switch tech stacks on a whim. I’ve convinced myself that there are significant benefits in changing to Hugo, and so I’m hopeful that I’ll stick with this new setup for the next several years.
I’ve found it unnervingly easy to shoot yourself in the foot with Jekyll’s templating system. It’s surprisingly easy to include something in a page template that causes page generation times to balloon. ↩︎
These posts always remind me of Erik Bernhardsson’s excellent “The eigenvector of ‘Why we moved from language X to language Y’” ↩︎