Sun Mar 18 2018 #blogging #hugo

Blog Migration

Table of Contents

I’ve migrated my blog from the Ghost blogging software to another program, called Hugo. I’ve used Ghost for some time, but it is missing features (like syntax highlighting) that are important to me; and they aren’t even on Ghost’s roadmap. The sooner I ditched it, the easier it would be.

Software Choice

I looked at Hugo and Jekyll. There are plenty of feature-by-feature comparisons out there, but the TL;DR is:

Hugo and Jekyll are both static site generators. They take your source files, templates, graphics, etc. and emit a directory you can upload to a dead simple hosting service. They also help you edit by giving you a local web server that refreshes itself (and your browser!) each time you re-save a file. Quite handy.

Hugo is a single-binary Go distribution. I’d like to learn Go and I want to deploy my blog by executing a server-less function, so these could be practical advantages for me in the future.

Jekyll is conceptually similar but has a much bigger ecosystem of plugins, themes, and hosting options. GitHub Pages uses Jekyll and you can use it (with a limited set of GitHub-blessed plugins) on your own GitHub Page for free.

After about a dozen hours working with Hugo, I decided my decision was probably wrong. I quickly ran into:

  • useless error messages
  • out-dated and confusing documentation
  • new and incomplete scheme for supporting assets (like images) in the same directory as post text
  • dodgy themes
  • the Go debugger, Delve, has a fragile installer which seems frequently broken on MacOS :(

However, I may have had just as many complaints about Jekyll if I had selected it instead. I decided to try Hugo for a few months and see if I can learn to love it.


Installing Hugo was easy. You can probably just download a binary for your OS directly from the project’s GitHub Releases page.

Before doing that, check their installation guide in case you prefer another method. I used brew install hugo on MacOS and it worked perfectly.

Blog Setup

The quick start guide is pretty good. There are a few things it doesn’t make clear, and some things you may want to know which are simply outside its scope (like git).

Prepare a Git Repo

I created a new blog repository on GitHub and cloned it into ~/blog.

jsw@athena:~$ git clone

Use Hugo to Populate Skeleton Directory

Since I’ve already created a git repository, I must use the --force option when invoking hugo new site. It won’t operate on a non-empty directory without the force flag.

Theme Installation

Hugo recommends cloning themes from their repositories into your blog directory, like: git clone http://... themes/casper-two.

If your blog’s content and configuration are themselves in a git repo, you should consider referencing your themes as git submodules instead.

If you don’t know how to use a submodule you should probably just download and unzip your themes and not worry about source control for them.

Note: I had a lot of trouble with the casper-two theme. I had to fork it and make some changes before I could really get my blog to work (tags pages were empty, for one thing.) I like the theme but it’s not turn-key.

Configure Theme Carefully

A theme may require parameters, like params.authorAvatar, that seem like they’d be optional. Instead of a helpful error message indicating that required config params are missing, your site may fail to build or appear to succeed but have no content, like this: where is everything

Broken Frontmatter Will Ruin Your Day

My first test post started something like the below example. It took me hours to realize you cannot terminate a YAML Frontmatter block with ... (the YAML way) and instead, must use --- at the beginning and end (a special Hugo way.)

title: a test post
image: foo.jpg
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt
This breaks your whole blog because the YAML is terminated with
three dots instead of three dashes!

Test Your Blog

Once I had overcome the above, I had everything needed for a test; basic config, theme installed, theme config, and a test post. I started up Hugo as so:

jsw@athena:~/blog$ hugo server

                   | EN  
  Pages            | 17  
  Paginator pages  |  0  
  Non-page files   |  8  
  Static files     | 12  
  Processed images |  0  
  Aliases          |  8  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 76 ms
Watching for changes in /Users/jsw/blog/{content,data,layouts,static,themes}
Serving pages from memory
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address
Press Ctrl+C to stop

Progress! Great!

Import a Legacy Post

I wanted my posts to each have their own directory. This makes it easy to keep images and other assets in the same directory as the post text. Hugo recently added this capability and it seems to be working nicely as long as your theme is prepared for it.

I set my blog repo up with the following directory structure:


I skipped a few extra directories and files, but the important part is the text content of posts are in files named and those files are in directories called blog/content/post/<title>/. I can also put images, PDFs, or whatever I want, into those directories to keep them organized.

The public/ directory is where output from Hugo’s “rendering” process goes. After running hugo i could upload that directory to any web host.

The static/ directory is just copied (by Hugo) into public/ when it runs. You should put things like a favicon.ico, logo graphic, proof-of-control files for your CDN, GitHub CNAMES file, or whatever, into this directory.

My permalinks configuration parameter determines the actual URL of posts; I have it setup like:

  post: "/:year/:slug/"

Legacy posts look like this:

# blog/content/post/mbp-charging-performance/
title: "MBP Adventure: Thunderbolt Charging Performance"
slug: MBP Charging Performance
  - /2018/02/24/mbp-adventure-charging-performance/
date: 2018-02-24T12:39:00-00:00
image: fig1-brief-test.png
tags: [apple, gadgets, thunderbolt]
toc: true
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt

The aliases directive makes sure posts are still reachable at their legacy URL so links from social media will continue to work.

Deal With Asset Locations

Finally, I needed to deal with some trouble caused by Hugo’s recent decision to support asset storage in post directories. Basically, the themes haven’t caught up to this, and it’s unclear to me if they really can yet.

To overcome this problem, I had to fork the theme I wanted to use and adjust it to support assets in post directories. You can see that change on GitHub. The hardest part was figuring out where to look.

I also needed to fix the image URLs being used in Facebook Opengraph and Twitter Card <meta> tags. Otherwise, social media wouldn’t show the right image if people share my posts. Here is the commit.

Finally, I added the publication date to posts. I think readers care if a post is current or if it might be out-of-date. The way the underlying Go library interprets date formats is ridiculous, therefore, the commit looks a bit ridiculous!

Produce Static Output

Once the blog was “working” locally, all I needed to do to “publish” is run hugo in the repo directory and upload the contents of public/ to some sort of web host.

Running hugo looks like this:

jsw@athena:~/blog$ hugo

                   | EN  
  Pages            | 46  
  Paginator pages  |  0  
  Non-page files   | 33  
  Static files     | 15  
  Processed images |  0  
  Aliases          | 25  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 74 ms

Setup GitHub Pages for Hosting the Static Content

GitHub Pages allows you to host a web site on GitHub’s servers. You can’t just upload files, though; you need to commit them to a git repo and push it to github.

In the case of “Personal Pages,” distinct from Project or Organization pages, github requires you to name the repo and it serves your site from the root directory of the master branch. You can customize those things for other types of Pages, but not Personal Pages. :-(

Therefore, I use two repositories, blog for source; and for output. The second one is basically an ephemeral repository; I could delete and re-create it without a care.

I’ve cloned into the public/ directory under my blog’s repo, and made sure the blog repo itself has public/ in its .gitignore file.

jsw@athena:~/blog$ git remote -v
origin (fetch)
origin (push)
jsw@athena:~/blog$ grep public .gitignore
jsw@athena:~/blog/public$ git remote -v
origin (fetch)
origin (push)

Repeatable Publication Process

Finally, all the setup tasks are complete, and we’re ready to publish new blog entries over and over. Here’s how we do that:

jsw@athena:~/blog$ hugo

                   | EN  
  Pages            | 49  
  Paginator pages  |  0  
  Non-page files   | 34  
  Static files     | 15  
  Processed images |  0  
  Aliases          | 26  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 76 ms
jsw@athena:~/blog$ cd public/
jsw@athena:~/blog/public$ git add .
jsw@athena:~/blog/public$ git commit -m "update blog"
[master (root-commit) 6e418b2] update blog
 147 files changed, 9047 insertions(+)
 create mode 100644 2017/06/18/virtualization-concepts-memory-segmentation/index.html
#..... lots more status messages about new files
jsw@athena:~/blog/public$ git push
Counting objects: 231, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (178/178), done.
Writing objects: 100% (231/231), 7.74 MiB | 1.50 MiB/s, done.
Total 231 (delta 73), reused 0 (delta 0)
remote: Resolving deltas: 100% (73/73), done.
 * [new branch]      master -> master

That’s it! For review, our repeatable process is just this:

  1. write content in the blog repo and commit changes
  2. from the blog directory, run hugo
  3. from the public directory, run git add .
  4. from the public directory, run git commit -m "any commit message"
  5. from the public directory, git push

Remaining To-Do


I started cleaning up and customizing the CSS, which had not been carefully maintained. There were many unused classes and the way text sizing had been tweaked over and over was utterly unmaintainable.

There’s more to be done. The Ghost Pro rendering of my blog does still look nicer. I’m a function over form guy, though; I’ll tidy up the look some other time.

Git-Triggered Publication Process

I plan to wrap Hugo up into an AWS Lambda so a repo commit can automatically trigger the publication process.

Markdown ToCs

Tables of contents are high on my list. I’m hoping the markdown processor Hugo is using will cooperate.

Photo Credits

I used an image from Unsplash in this post. I included the photographer’s information in my Frontmatter data so I can have my theme display a photo credit.

image: harshil-gudka-556417-unsplash.jpg
image-credit: Photo by Harshil Gudka on Unsplash

I need to implement the above in my theme.