Migrating to Hugo

New look? Yup! I’ve migrated to Hugo. Read on to learn how.

I’d been meaning to migrate all of my sites to Hugo for a while. I’ve been a WordPress user for many, many years, but the growing bloat of WP combined with its numerous security issues–not to mention the endless treadmill of plugin updates–led me to consider alternatives. My hand was forced by a recent database corruption. Rather than spend a bunch of time trying to recover a corrupted database, I figured it would be best to take my current backups and migrate to Hugo once and for all.

This is not to say I’d never migrate to something else, but a major benefit of moving to Hugo is that all of my posts are now in Markdown format, allowing me to track them in git the way I do everything else I write. Consuming Markdown files will be a requirement for any CMS I use in the future, too.

Exporting a WordPress Site

There are various ways you can export a WordPress site, but I’ll simply describe the method I used. I used the built-in Export system (under Tools -> Export) to generate an XML file.

I did this for each of my sites, and processed them through exitwp. This process generates a directory called build, then breaks down further directories based on each site being exported, such as jdhcreates.com. Under each site directory are directories like _posts. You might have others–I only exported my posts, as that’s all I really cared about.

What’s important here is that I ended up with all of my posts in Markdown format in the _posts directory. These are used to seed Hugo later on.

Installing Hugo

I’m running Raspbian (Debian) Linux on a Raspberry Pi, with nginx as my web server. Setting those up is beyond the scope of this yada yada. Installing Hugo itself is fairly easy, though.

Assuming you are on a Linux system using aptitude for package management, you can do this:

cd /tmp
curl -LO https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-ARM.deb
sudo apt install ./hugo_0.55.6_Linux-ARM.deb
sudo cp /usr/local/bin/hugo /usr/bin/

You’ll need to replace the URL above with the right release for your system.

Installation for other platforms is described here.

You can be sure you have Hugo installed correctly by running hugo version, which will output something like this:

Hugo Static Site Generator v0.55.6-A5D4C82D linux/arm BuildDate: 2019-05-18T07:57:00Z

If you get an error message instead, refer to the installation instructions!

Converting to Hugo

Remember the _posts directory from before? We’re gonna need that now!

First, pick a location to set up your Hugo sites in. It can be anywhere, though preferably a directory under /home. Once you know where you want to set up a site, you’ll run:

hugo new site [sitename]

Replace [sitename] with the directory name you want for your site. Then, cd into it.

The easiest thing I found to do was to copy all of _posts (from exitwp) into [sitename]/content/posts.

You’ll also need to pick a theme. Choose one you like from the official Hugo theme gallery. I’ll note that I had trouble getting some of them to work, but ones that have worked well for me are:

Let’s say you picked Binario. Make sure you’re in the directory for [sitename], then run the following to set it up:

cd themes
git init
git submodule add https://github.com/Vimux/Binario.git
cd ..
echo 'theme = "Binario"' >> config.toml

You’ll need to edit config.toml to set your site URL and other data, too.

Suppose you have static content from your old WP installation that you want to bring over. That’s easy enough, too! Locate your /wp-content/uploads directory and copy it to the static directory for your Hugo site. To be clear, you’ll want static/wp-content/uploads to exist under your Hugo site, containing all the files your WP installation had in the same location.

Are you ready to generate your site? Easy! Make sure you’re in the directory for your site (it should have content, themes, and so forth below it), then run:


That’s it! You should get output like:

Building sites … 

                   | EN
  Pages            |   6
  Paginator pages  |   0
  Non-page files   |   0
  Static files     | 122
  Processed images |   0
  Aliases          |   0
  Sitemaps         |   1
  Cleaned          |   0

Total in 621 ms

Again, if you get errors, first check to see if they relate to your theme. If so, try another one. I don’t bother trying to fix a bad theme–I just get a new one. YMMV if you happen to find one you really like!

Once your site is generated, you’ll want to publish it somewhere. First things first: you should move your existing WordPress directory elsewhere, such as by renaming it with a _old suffix. Don’t deploy your Hugo site directly on top of an existing WordPress installation! No telling what might happen, but you sure won’t like it.

Assuming you are on the same server where you have been working with Hugo so far, all you need to do next is copy what’s in the public directory to your (now hopefully empty) web root, which will often be a subdirectory under /var/www. After you’ve copied these files, browse to your website URL and see if it’s working correctly now. If not, make sure you have copied your public directory to the right location and double-check that you aren’t looking at cached files from your WordPress installation.

Up to this point, you will have migrated an existing WordPress site over to Hugo! Great job!

Of course, you might want to be able to update it too, right?

Automatic Site Generation

I’ll admit it: I don’t care for Hugo’s method of running a local server to test out new content. I’d rather just write my Markdown file, put it in git, and let some kind of automation handle generating the website from there.

To that end, I wrote a Python script which takes a manifest of websites and a path to a local git repository and simply builds everything from that. I run this on a cron schedule and the websites just generate automatically on that schedule, picking up any new or changed files. (It doesn’t delete any, so you’ll have to handle that yourself, or modify the script to do it for you.)

There are plenty of resources out there to tell you how to set up and use a git repository, so I won’t explain that here. I’ll just assume you have one already and have it cloned to a system you can use. Create a directory in your repository called hugo or whatever you like.

Create a file called sites.txt in the hugo directory, and put the following at the top of it:


This is just a reminder for the file format. Yes, it’s pipe-delimited and I’m showing my age. Sue me.

Every subsequent line should contain a pipe-delimited description of the site to be generated. For this site, it is:

jdhcreates.com|JDH Creates|jdh-blog|https://github.com/josephhutch/aether.git|JDH is Creating!|The latest stories, music, code, and et cetera...

It’s OK if you have just one! I have several and I wanted to run them all from the same place.

Note that brand and desc are only part of certain themes, such as the aether theme I use here. Those fields are optional.

writepath is not very obvious either, is it? It’s really just the subdirectory within your git repository where all your Markdown files for the designated site can be found. Nothing more or less! Easy peasy, right? Please note that your wp-content directory is expected to be inside the directory you used for writepath, as well, if you have one. (If not, it will just be ignored.)

Now, the script! Save it as hugogen.py or whatever you like–again, put this alongside sites.txt.

import os,sys

repo = '[path to local copy of repo]'
hugopath = '[path to set of hugo sites]'
wwwpath = '[path to webroot for all sites]` # Generally: /var/www

# Run git pull on the repo.
os.system('cd ' + repo + ' && git pull')
# Process sites.txt
sites = open('sites.txt','r').readlines()
for s in sites:
    domain, title, writepath, theme, brand, desc = s.strip().split('|')
    if not os.path.exists(hugopath + '/' + domain):
        os.system('hugo new site ' + domain)
    os.chdir(hugopath + '/' + domain + '/themes')
    os.system('git init')
    os.system('git submodule add ' + theme)
    os.chdir(hugopath + '/' + domain)
    config = open('config.toml','w')
    config.write('languageCode = "en-us"\n')
    config.write('baseurl = "https://www.' + domain + '"\n')
    config.write('title = "' + title + '"\n')
    themename = theme.split('/')[-1].split('.')[0]
    config.write('theme = "' + themename + '"\n')
    if brand != '' or desc != '':
        config.write('brand = "' + brand + '"\n')
        config.write('description = "' + desc + '"\n')
    if not os.path.exists(hugopath + '/' + domain + '/content/posts'):
        os.mkdir(hugopath + '/' + domain + '/content/posts')
    if not os.path.exists(hugopath + '/' + domain + '/static/wp-content'):
        os.mkdir(hugopath + '/' + domain + '/static/wp-content')
    os.system('cp ' + repo + '/' + writepath + '/* content/posts/')
    os.system('cp -a ' + repo + '/' + writepath + '/wp-content/* static/wp-content')
    os.system('cp -a public/* ' + wwwpath + '/' + domain + '/html/')

To run this in a daily cron job, you’d run crontab -e and add this entry:

0 8 * * * python [path to hugogen.py]

Obviously, you’ll have to add your own path since I don’t know what it is. :)

To add new content, you’d just add new Markdown-formatted posts to the writepath of the proper site, then run the Python script (or wait for the schedule to run it). Just be sure to commit them back to git and push them to your remote repository, if you have one!

Another way to handle this would be webhooks that fire whenever your git repository updates. There are a bunch of ways you could automate generating your Hugo sites, this is just the way I’ve chosen as it’s the most familiar to me.

I’ve also not mentioned how to handle comments. The truth is, comments were so seldom used on my sites that I didn’t care much about keeping them. I do, in fact, still have them, but I may not take the time to import them. That said, adding comments to your Hugo sites is easy. There are several methods here, any of which should be good.