Blog Plumbing

I recently migrated my blog from Jekyll and GitHub pages to Hugo and Netlify and figured I would take the opportunity to write up my experiences. Blog plumbing isn't directly related to security, but I'm a proponent of having one and if you're going to, you could do far worse than a static site hosted on someone else's hardware.

I am a big fan of static sites. They're fast, secure, and simple. Static site generators managed to make them even better. There are lots of them, but the general idea is the same across all of them: take some form of structured input, usually Markdown, and generate static HTML pages. When you can take untrusted user input out of the equation, you're in a good place.

I hadn't quite justified the move to myself before I started. I think I'd settled on "well let's just take a poke" and then suddenly the day was gone. My old setup was fine. Totally fine. The biggest gripe I had was installing Ruby on every machine I blogged from and advertising to the world that it sometimes takes me 11 commits in five minutes to get a new post right.

There are reasonable, light-touch solutions to both of these problems. Docker and a bit of proofreading, for example, but if I'm being honest I also wanted to fiddle with a new tech stack, and Netlify has all sorts of goodies.

I'll cover install and configuration of Hugo and Netlify and touch on some of the design-related stuff you'll need to contend with if you decide to roll your own theme (or modify someone else's).

Installation & Configuration

I hacked my Jekyll site together from a variety of themes and random internet resources but wanted to take a more planned approach this time so I decided to wrap everything up in a theme.

Hugo's quick start documentation is quite good and I generally followed that, modifying as needed to accommodate my shiny new theme.

The install process for Hugo on Linux is simple:

1
2
$ apt install hugo
$ hugo new site <NAME>

You could say it is deceptively simple. And you'd be right.

Theme

I followed a great guide that covered the basics, and then reimplemented most of the structure and layout of my last site. Luckily, I have a rather spartan blog so this wasn't too tricky.

One thing to note is that any paths referenced in your theme are relative to the theme's root directory (themes/<NAME>), not your site's.

One area I spent some time getting confused is the navigation menu (about, contact). I'd originally hardcoded links to the relevant pages and was struggling to do the same with Hugo (something something no HTML in Markdown) until I realized this can be configured in the config.toml with way less effort:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[permalinks]
  post = "/:filename/"
  
[menu]
  [[menu.main]]
    name = "about"
    title = "About"
    url = "/about/"

  [[menu.main]]
    name = "contact"
    title = "Contact"
    url = "https://keybase.io/liso"

The About page lives under content/about.md and the [permalinks] config tells Hugo to use the filename when generating the path; about.md becomes /about/.

I then added this to my header partial:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<div class="header__nav">
    {{ $list := (.Site.Menus.main) -}}
    {{ $len := (len $list) -}}
    {{ range $index, $element := $list -}}
        {{ if eq (add $index 1) $len -}}
            <a href="{{ .URL }}" title="{{ .Title }}">{{ .Name }}</a>
        {{ else -}}
            <a href="{{ .URL }}" title="{{ .Title }}">{{ .Name }}</a> | 
        {{ end -}}
    {{ end -}}
</div>

Hugo's syntax takes some getting used to, but this just loops through all the items in the main menu and adds a link. If it's the last link, it won't add the trailing | delimiter.

Quote Wrangling

I love a good quote, but I don't love formatting them manually, so I created a really simple custom shortcode to handle this:

1
2
3
<blockquote>
    "{{ .Get "text" }}"{{ with .Get "author" }}<span class="blockquote__author"> — {{.}}</span>{{ end }}{{ with .Get "url" }} {{ .| markdownify}} {{ end }}
</blockquote>

The with first checks for the existence of a value and the following {{.}} inserts it. We can provide an author, a URL, or both when creating a quote. The | markdownify lets me use Markdown's link syntax.

Design

I appreciate good design but I'm no designer. What they do is like magic to me. I lucked out when building my original site and have been happy with the result for years. Writing that, I can't think of much else on the tech front for which this is true. I decided to stick with it but to simplify wherever possible.

I had a lot of crufty cruft in my old CSS and decided that a migration was the perfect time to deal with it. First up was naming my selectors. Naming things is always tricky, but BEM seems to have a sane approach, so I went with that.

I also started out without any of the custom padding, margins, and line spacing. It turns out the defaults look pretty good! Much ink has been spilled on how to tweak these if you decide to break some trail here.

CSS vs. SCSS

I initially decided to avoid a dependency on SCSS and use CSS custom properties instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
:root {
  --font-family: "Verdana";
  --font-color: #424242;
  [...]
}

body {
  font-family: var(--font-family);
  color: var(--font-color);
  [...]
}

This was promising until I realized that unlike SCSS, the variables would be there in the finished product, so if you were inspecting the page you'd have to manually look up what each value was. Nothing makes me crankier than confusing CSS. That is a lie.

I followed much of this when sorting out colour and structure and found the support for creating hues really helpful:

1
2
3
4
5
// Colours                                          
$main-color: #2a7ae2;                               
$light-gray: #fdfdfd;                               
$dark-gray: mix($main-color, #333333, 10%);                                                              
$medium-gray: mix($dark-gray, $light-gray, 50%);

I don't even need to tell you what any of this does. Isn't that neat?!

Hugo's support for code blocks is great; the style you see used here is 100% stock. This further simplified my already slimmed down stylesheet. I also took this opportunity to grep through all my old posts and make sure I used the right language in my code blocks.

One thing that tripped me up during the retreat to SCSS was that while CSS files live in the static/ directory, SCSS files must go under assets/sass.

(Mostly) Platform-Agnostic Fonts

Simplicity in all things was a goal, so I opted for pre-installed system fonts rather than sourcing Google fonts from who-knows-where every time the page loads. This reduced my choices from approximately a billion to a dozen or so.

I started by picking a font I liked the look of and then found ones that were roughly equivalent in size and spacing for each platform:

1
2
3
$sans-serif: "Helvetica Neue", "Liberation Sans", "Arial", sans-serif;
$serif: "Georgia", "Bitstream Charter", serif;
$monospace: "Courier New", "Liberation Mono", monospace;

Home Row or Bust

I'm a vim convert and consequently get irritated when I need to take my fingers off of home row. To more quickly review the effects of my tinkering on old posts, I added an access key to the Next and Previous post links. You can now navigate using ALT + n and ALT + p, respectively.

RSS

I had to make a few small changes to the default RSS config. The easiest way to do this is to copy Hugo's base template to themes/<NAME>/layouts/_default/rss.xml.

By default, all pages will be included. I just want my posts going out and this article set me on the right path. I also enabled full RSS content by changing all instances of Summary to Content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{ range (where $pages ".Section" "post") }}
<item>
  <title>{{ .Title }}</title>
  <link>{{ .Permalink }}</link>
  <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
  {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
  <guid>{{ .Permalink }}</guid>
  <description>{{ .Content | html }}</description>
</item>
{{ end }}

I still can't find any documentation on the | html function, but I can confirm it's absolutely necessary for the sane formatting of your content. Mine was initially broken on Feedly, which lead to tinkering, which lead to further breaking, which eventually lead back to what you see above and now everything is fine.

Finally, I renamed the default index.xml feed to feed.xml so my 12 subscribers wouldn't lose their subscription. Hopefully you all made it!.

This was pretty easy:

1
2
3
4
[outputFormats]
  [outputFormats.RSS]
    mediatype = "application/rss"
    baseName = "feed"

Netlify

At some point over the last year or so, Netlify entered my consciousness as the inevitable target for the eventual migration. I am happy to report that my on-a-whim decision was bang on.

Netlify has a generous free tier. I like having options, and the ability to rapidly scale is as appealing as the need to is unlikely. They've also got all sorts of DevOps goodies baked in which I am excited to play with since most of my professional life is concerned with breaking, not building or maintaining.

The Hugo guide is exactly what you should be reading if you're deploying a Hugo blog to Netlify. Also, you should read it before you start, not halfway through your broken deploy like I did (P.S. remember to set your Hugo version).

Netlify had no problem accessing my private GitHub repository, but choked on the private submodule I'd included for my theme. The workaround was pretty simple; I just needed to configure GitHub deploy keys on the theme's repo, which are exactly what they sound like.

Once everything was deployed, I updated my CNAME, removed Cloudflare's universal SSL (Netlify supports LetsEncrypt), and poof! There we were. And here we are. Together.

Epilogue

Search did not make it. I miss it a little, but not enough to wade into that particular swamp of JavaScript just yet.

<<