Moving From Wordpress to Hugo

I decided to change things up for my blog again, after getting restless with Wordpress. I wanted to play with some new tech, and here I am.



  • Minimum of dependencies needed to write and publish posts.
  • Play around with some of that new cloud tech the kids are talking about now.
  • Free? Given the low volume of my blog, I think this is achievable.


  • Pay as you go. Blaze plan allows you to host multiple sites and SSL provided by Let’s Encrypt, paying only for usage over their ‘free tier.’
  • Git-based version control. All the posts are text editable.


  • Losing browser-based editing and publishing.
  • If I don’t push in-progress posts to the private Git repo as I write them, that content is only stored on the machine which I was writing it.

So how does the new system work?

Create accounts

Create a Github account and create two repositories, one for the raw Markdown content, images, themes. Another for the generated website. It’s up to you whether the first one is public or private, but the generated website repository must be public. Why? Travis CI needs access to it via webhooks to trigger a built upon every commit.

Using your Github credentials, create a Travis-CI account, and sync your repositories to it. Don’t worry, all this is free unless your blog gets really popular and you update content a lot.

Finally, create a Firebase account and upgrade to the Blaze account type. This uses ‘pay as you go’ pricing with a decent free-tier that will help you get up and running and judge whether to jump to the next pay tier if the site gets more popular.

Writing content

Create your blog using the instructions on the Hugo quickstart.

Depending on how you want to set up your Git repositories there are several ways to do the next part.

I created a private Github repository for the raw blog site, where I can do my endless commits and fixes before content goes public. I then created a public Github repository for the generated HTML content, images, etc. This public repository is a submodule of my raw blog site directory, where I and commit the generated content.

After I am done writing the post in markdown, I run the following script to commit the changes to both private and public repo, and then submit everything.

#!/bin/bash -x

if [ "$#" -lt 1 ]; then
    echo "Error: Forgot to specify an environment: test or prod"
    exit 1

if [ "$#" -ne 1 ]; then
    COMMIT_MSG="generic commit on `date`."

echo "Committing and deploying to $HUGO_ENV"
echo "Commit message: $COMMIT_MSG"


cd "$BASEDIR/blog/public/blog-$HUGO_ENV"
git submodule update --remote --merge 


# # create the content
$BASEDIR/bin/hugo --environment $HUGO_ENV -s $BASEDIR/blog

# # commit the changes in the private repo
git commit -m "commit to $HUGO_ENV: $COMMIT_MSG"
git push origin master

# # # commit the changes in the $STAGE public repo
cd "$BASEDIR/blog/public/blog-$HUGO_ENV"
git add .
git commit -m "deploy to $HUGO_ENV: $COMMIT_MSG"
git push origin master

Deploying Content

After submission of the blog content to the public Github repository, travis-ci is used as the continuous deployment solution to get my changes pushed to Firebase. Why?

I wanted to reduce the number of local dependencies required to publish Blog content. The bare minimum: an editor, the Hugo binary, and access to the private Git repo that houses my content. If I wanted to deploy directly from my machine to Firebase it would require nodejs installed (Firebase’s tools require it), plus the Firebase API keys for my account. Often the machine I create personal content on has restrictions about storing credentials, and so I’d rather avoid that.

Back to the workflow.

Generating Credentials

To allow travis-ci access to your Firebase hosting account, you need to create API credentials. Not wanting to install nodejs on my machine itself, I fired up a nodejs container:

hostmachine$ docker run -it --rm node:11-alpine /bin/ash
container# npm install -g firebase-tools
container# firebase login --no-localhost
container# firebase init
(select hosting)
container# firebase login:ci --no-localhost

The --no-localhost flag is important. These commands normally set up a locally running httpd to verify your identity. Since you ran the commands inside a container, ‘localhost’ is within the container, and NOT your hosts interface. It was more work than it’s worth doing all the port forwarding, and –no-localhost solves that problem completely.

Save the token returned from the login:ci invocation. You’ll need it when you set up your travis-ci repository.

I used all the default options on my repo’s configuration in travis-ci, and added the below specified environment variables to be able to perform the actual deploy.

The following three files should be located in the base directory of your public Git repository.


language: node_js
  - "stable"
  - echo "Deploying!"
  - npm install -g firebase-tools
  - firebase deploy --project $FIREBASE_PROJECT --token $FIREBASE_TOKEN --only hosting:$TARGET

This travis-ci configuraiton uses a node_js container, installs the firebase-tools, and then runs a hosting-only deploy of the content to the configured project and host target.


  "projects": {
    "prod": "formannet-infra"
  "targets": {
    "formannet-infra": {
      "hosting": {
        "blog-prod": [

This JSON file configures the project associated with your repository, along with the targets of the hosting deploy. (This was one of the more confusing parts of the deploy. If you only have one site per Firebase project, you don’t need a .firebaserc file)

In this instance:

  • FIREBASE_PROJECT: My firebase project with the hosting setup, specifying the FQDN for the Hugo site.
  • FIREBASE_TOKEN: Token generated by ‘firebase login:ci’, allowing for human-uninvolved deploys.
  • TARGET: The specific site configured in my Firebase project to deploy. This allows me to specify a staging or prod deployment target. (In this case, blog-prod)


  "hosting": {
    "target": "blog-prod",
    "public": "blog",
    "ignore": [

firebase.json configures which directory is used for the deploy (in this case, blog), for the target blog-prod, excluding the listed files from being deployed.

Documentation for the configuration files can be found at

An image of travis-ci deploying a blog update:

Image of TravisCI deploy workflow

You have now, with two Git commits, deployed a blog update to your Firebase-hosted website without any of the other expensive dependencies of deployment. And you even get an email once deployment has completed.



Jeffrey Forman
Jeffrey Forman

I do things that make the Internet work at work, and I play around with things that make the Internet work at home.