Goodbye Gatsby, Hello Hugo

Hugo has ruined me. Other static site generators that rely on community plugins and copypasta to fill out their gaps don’t cut it anymore…

Updated on Read in 11 mins
Illustration of the Gatsby logo with a red line through. Next to it is the Hugo logo underlined in yellow.

Personal sites are a great testing ground for mixing it up and experimenting. Back when Gatsby first started buzzing in Jamstack circles, I used that as my excuse to migrate off Jekyll and learn something new.

As someone who avoids writing JavaScript whenever possible, there were considerable growing pains as dug into the React, GraphQL, and NPM ecosystems. It took time, but I got the site to a place on par with what I had achieved with Jekyll.

As I used Gatsby more, the reliance on plugins started frustrating me. External dependencies lagging behind Gatsby made updating a chore, and in the case of migrating to Gatsby 5 — impossible.

For example, critical remark plugins related to Markdown parsing became stuck blocking me from upgrading, unless I wanted to stop using them… which I didn’t.

Static site generator choices

So I started looking around. Jekyll seemed like a logical choice, but returning to Ruby… no thanks. Not to mention the appearance of Jekyll fizzling out wasn’t doing it any favors.

Eleventy (11ty) is what all the cool kids are using, and it shares a lot of Jekyll-isms. And then there is Hugo, which made a splash years back when Smashing Magazine switched from WordPress to it. Maybe these were good static site generators to explore?

For a few weeks, I worked on a proof of concept, migrating layouts and content from Gatsby over to 11ty. It all felt Jekylly but with added flexibility and none of the Ruby pain.

Then I encountered a roadblock (more of a detour) with collections and how 11ty uses tags to assign them. I’m sure I could have worked around it, but conceptually it clashes with how I organize content: categories as sections > tags for loosely related topics.

On the other hand, Hugo seemed purpose-built for me. Full of strong conventions on how to do things, but with enough flexibility to bend as needed. This was a stark contrast with what I had experienced trying to organize and write content with Gatsby, 11ty, and to a lesser degree Jekyll. With those SSGs, I often felt like I was rolling my own solution to do something that should be standard.

Feature comparisons

Want to dump a bunch of Markdown files in a folder with a sprinkling of front matter… you can. But if you want to level up, Hugo offers up ways of bundling pages into sections alongside resources so you can keep your content nice and tidy.

When following conventions Hugo expects, the file structure is used to inform a page’s section, content type, whether a branch or leaf node, its layout, and more. Making the building of menus, breadcrumb navigation, list pages, and pagination straightforward as they are fully integrated into Hugo’s core features.

Building basic blog sites with posts listed in reverse chronological order, it’s easy to miss out on all that Hugo offers. And to be honest, unless you have prior experience with static site generators or fully read through Hugo’s (at times obtuse) documentation, you can miss out on the benefits of having the following features.

Markdown render hooks

Coming from Gatsby, I now expect all static site generators to take image references written in Markdown and spit out resized versions with appropriate responsive srcset markup.

I give you this:

![Chicken wings covered in hot sauce](chicken-wings.jpg)

You give me this:

<img alt="Chicken wings covered in hot sauce" src="chicken-wings.jpg" srcset="chicken-wings-300.jpg 300w, chicken-wings-600.jpg 600w, chicken-wings-900 900w" sizes="(max-width: 900px) 100vw, 900px" loading="lazy" decoding="async" width="900" height="600">

I fought with Jekyll for years trying to do this. The common pitfalls are:

  • Image assets live outside of your _posts or collection folders. Linking to them with relative paths in Markdown files is more difficult than it should be.
  • Every Jekyll responsive images plugin I tried used some flavor of ImageMagick to convert images. ImageMagick is slow and slogging through thousands of files crushed my build times. You can sort of get around this by using a combination of Sharp and Gulp, but it hardly feels like the Jekyll way.
  • Using Liquid tags e.g., {% responsive_image path: chicken-wings.jpg alt: "Chicken wings covered in hot sauce" %} is less than ideal if you’re committed to an all-Markdown workflow. A pre_render hook plugin that replaces Markdown image references with an appropriate {% responsive_image %} tag can get around this, but feels hacky.

11ty with the Sharp-based eleventy-img plugin and a custom image renderer for markdown-it can get you closer. Still, for me, there was enough flakiness going on — missing images and random build crashes — encouraging me to look elsewhere. It’s an open issue, so here’s hoping that someday there will be an enhancement to improve it.

Then there’s Hugo, which lets you customize how Markdown outputs to HTML with what it calls render hooks. These render hook templates are standard HTML with the same templating logic used to write shortcodes, partials, and layouts. Something I appreciate, as writing HTML comes more naturally to me than JavaScript or Ruby.

I’m using a render hook (render-image.html) to process Markdown images into responsive image markup like so:

{{- $sizes := (slice "300" "768" "1024" "1280") -}}
{{- /* Get file that matches the filename as specified as src="" */ -}}
{{- $src := .Page.Resources.GetMatch .Destination -}}
{{- if and (not $src) .Page.File -}}
  {{- $path := path.Join .Page.File.Dir .Destination -}}
  {{- $src = resources.Get $path -}}
{{- end -}}
{{- $alt := .PlainText | safeHTML -}}

{{- /* Conditional for resources that aren't in page bundle. */ -}}
{{- if $src -}}
  {{- $color := delimit (first 1 $src.Colors) "" -}}
  <img sizes="(min-width: 1440px) 690px, (min-width: 1024px) 790px, 94vw"
    srcset="
    {{- range $sizes }}
      {{- if ge $src.Width . }}{{ ($src.Resize (printf "%sx" .)).RelPermalink }} {{ (printf "%sw" .) }}, {{ end }}
    {{- end -}} {{ $src.RelPermalink }} {{ $src.Width }}w"

    {{- /* If smaller than 768px wide, then load the original */ -}}
    {{ if ge $src.Width "768" }} src="{{ ($src.Resize "768x").RelPermalink }}"
    {{ else }}src="{{ $src.RelPermalink }}"
    {{- end -}}
    alt="{{ $alt }}" loading="lazy" decoding="async" width="{{ $src.Width }}" height="{{ $src.Height }}" {{ if ne $src.MediaType.SubType "png" }}style="background: {{ $color }}"{{- end -}}
  />
{{- else -}}
  <img src="{{ .Destination | safeURL }}" alt="{{ $alt }}" loading="lazy" decoding="async" />
{{- end -}}

Add anchor links (render-heading.html) to headings:

<h{{ .Level }} id="{{ .Anchor | safeURL }}">
  <a href="#{{ .Anchor | safeURL }}" title="Permalink to {{ .Text | plainify }}">{{ .Text | safeHTML }}</a>
</h{{ .Level }}>

And add a rel="noopener" attribute to external links (render-link.html):

<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} rel="noopener"{{ end }}>{{ .Text | safeHTML }}</a>

Archetype templates

I write new posts so infrequently that it’s not uncommon for me to forget the names of front matter fields used in site layouts. To get around this I clone a starter Markdown document with a pre-configured placeholder front matter and draft a new post from that.

Hugo improves on this with the concept of archetypes — directory-based template files used for creating new content.

Archetype templates look a lot like one of my starter .md files but with template logic and variables to pre-populate front matter fields like title and date.

When running the hugo new command to create a new file, an appropriate archetype is used based on the document’s file path. This flexibility allows you to have different sets of archetype templates based on content type or section.

My /archetypes/default.md template looks like this:

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
last_modified_at:
draft: true
description:
image:
toc:
---

Which when used with hugo new notes/my-new-note.md generates a new Markdown file with the following starter contents:

---
title: "My New Note"
date: 2023-01-12T14:04:39-05:00
last_modified_at:
draft: true
description:
image:
toc:
---

The simplicity of creating templates in the same YAML and Markdown format as the rest of the site content makes a ton of sense to me. While other static site generators require: a content management system, scripts, task runners, or in Jekyll-land the jekyll-compose Ruby gem — to do what Hugo does natively.

Taxonomies

If you use taxonomies like categories or tags to group content, the popular SSGs have you covered as Jekyll has collection support to do custom grouping, but it’s not as robust as what Hugo offers.

11ty also has collections but confusingly calls them tags, which is inconvenient if you’re coming from Jekyll or any other blogging platform.

Gatsby has no concept of categories or tags and it is up to you to structure, organize, assign relationships, and write custom logic to filter content. I had a hell of a time writing my gatsby-node.js file to generate paginated archive pages for categories and tags.

And then there is Hugo, which automatically creates taxonomy pages for categories and tags. If that’s not enough for you, Hugo also natively supports:

  • Creating custom taxonomies to handle relationships other than categories and tags.
  • Ordering of taxonomies by weights allows the same piece of content to appear in different positions in different taxonomies.
  • Adding metadata to a taxonomy or term, something I used to hack together with YAML data files in Jekyll and Gatsby.

Content excerpts

Gatsby, Jekyll, and Hugo all have support for automatically creating shortened excerpts of a page’s main content to be used as teaser/preview text in RSS feeds, meta descriptions, and the like. Each of these SSGs uses separators to mark what part of the content should be used as an excerpt.

11ty requires front matter customizations to parse excerpts and separators from content to function how the three SSGs above do.

Image processing

Gatsby, Hugo, and 11ty all support image processing. Gatsby has the advantage if you’re using components as it can do responsive images with trendy loading effects like blur up, background color, and traced SVG via props and GraphQL queries.

On the flip side, if you’re bringing your own HTML and need a utility to transform and manipulate images — Hugo and 11ty have you covered.

The differences come down to how images are processed and rendered:

  1. Gatsby has React image components and the magical black box that is gatsby-remark-images.
  2. 11ty has eleventy-img which can be used in layouts and shortcodes.
  3. Hugo has image filters you can weave into layouts, shortcodes, and render hooks.

No surprise here, but I prefer the Hugo way. Using variables and piping on filters feels more natural than GraphQL queries and JavaScript. For example, a feature image defined in the front matter can be transformed (resized to 600px wide) like so:

{{ $source := .Page.Resources.GetMatch .Params.feature }}
{{ $image := $source.Resize "600x" }}
<img src="{{ $image.RelPermalink }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="">

Adjusting an image’s hue, brightness, sharpness, and adding overlays are a set of filters away with Hugo.

Colors

I started using Hugo’s new colors method to return the dominant colors of an image. I then limit this array to the first value and set it as a background-color to serve as a low quality image placeholder (LQIP) before it fully loads.

Don’t worry, I’m also setting a width and height on all images to avoid layout shifts during load.

Screenshot of gallery page with loading images showing solid backgrounds

To encourage browsing, I include links to related content at the end of each post. I’ve managed these groupings and relationships:

  1. Manually in the front matter.
  2. Automatically with plugins.

Thankfully, both Hugo and Jekyll have built-in methods for accessing related content automatically.

Hugo can list a page’s related content based on the front matter parameters you define. While Jekyll’s related posts module uses classifier-reborn to create relationships, is limited to the posts collection, and is slow.

Each can be looped through using Go and Liquid:

Related posts via Go:

{{ $related := .Site.RegularPages.Related . | first 3 }}
{{ with $related }}
  <h3>Related posts</h3>
  <ul>
    {{ range . }}
      <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
    {{ end }}
  </ul>
{{ end }}

Related posts via Liquid:

{% if site.related_posts.size >= 1 %}
  <h3>Related posts</h3>
  <ul>
    {% for related_post in site.related_posts limit: 3 %}
      <li><a href="{{ related_post.url }}">{{ related_post.title }}</a></li>
    {% endfor %}
  </ul>
{% endif %}

With Gatsby, I struggled with GraphQL queries and an earlier version of gatsby-remark-related-posts to automatically determine related posts. From the documentation, version 2 of the plugin seems to have improved on the clunkiness. But I can’t verify if that’s true as I haven’t tried upgrading yet.

11ty didn’t appear to have a native way of listing related content. Methods I dug up involved manual effort and maintenance — add tags to content in the front matter then filter on those tags.

Automatic table of contents

Gatsby’s remark transformer, Jekyll via a Kramdown {: toc} reference, 11ty plugin eleventy-plugin-toc, and Hugo all take Markdown files and generate a table of contents from the headings. Useful for creating jump links to skip lengthy bits of content.

Conclusion

If it wasn’t clear already — I ditched Gatsby and migrated this site over to Hugo. Hugo is often viewed as the fastest static site generator… it is.

It excels at content management, with a flexible organizational structure, multilingual mode, and menu system that all integrate with one another. It is clear that Hugo’s maintainers carefully consider each feature and how they should work together.

Hugo has ruined me. Other static site generators that rely on community plugins and copypasta to fill out their gaps don’t cut it anymore…

Related

Twenty Nineteen

First time doing a year in review post. The statistics contained are just for fun as there’s nothing to compare them against yet.