Hosting a Hugo blog on GitHub Pages with Travis CI

This post describes how to set up a blog using Hugo, an open-source static site generator. The blog is hosted on GitHub Pages, a web hosting service offered by GitHub. The Travis CI continuous integration service is used to deploy changes to the blog.

This post is based on Artem Sidorenko’s article Hugo on GitHub Pages with Travis CI.



Running a Hugo blog on GitHub Pages requires you to set up two GitHub repositories:

  • The first repository is named blog and holds the Hugo sources.
  • The second repository is named and holds the generated content.

(Throughout this post, replace username with your GitHub username.)

You also need to set up Travis CI such that, when you push a change to blog, it invokes Hugo to rebuild the site, and pushes the generated content to GitHub Pages will then deploy the site to

Installing Hugo

Installing Hugo on macOS is easily achieved using Homebrew:

brew install hugo

See Install Hugo for alternatives.

Setting up the blog repository

In this section you set up the blog repository on GitHub.

Creating the blog

The first step is to generate the files for the new Hugo site:

hugo new site blog

Creating the repository

Initialize a git repository in the newly created directory, and create the initial commit:

cd blog

git init
git add .
git commit -m "Initial commit"

Installing a theme

The next step is to install a theme. For now, stick with the ananke theme recommended in Hugo’s Quick Start tutorial.

git submodule add \ \
git add .
git commit -m "Add submodule themes/ananke"

This command adds the ananke git repository to the themes subfolder. Using a git submodule has the advantage of allowing you to track upstream changes to the theme.

Configuring the site

Hugo is configured using a file called config.toml, which has already been generated for us. Edit this file to set the site URL and the blog title, and to declare the theme you just installed. This is what the file should look like:

baseURL = ""
languageCode = "en-us"
title = "My Blog"
theme = "ananke"

Commit your changes:

git add config.toml
git commit -m "Configure site"

Publishing the repository

You are now ready to publish the blog repository to GitHub. One convenient way to do so is using hub, a command-line tool for managing GitHub repositories:

brew install hub
hub create
git push origin master

Setting up the repository

In this section you set up another repository, named, for the static content generated by Hugo. GitHub Pages deploys the repository automatically to the site located at

Creating the repository

Let’s start by creating the repository locally:

echo "#" >

git init
git add .
git commit -m "Initial commit"

Note that you created the repository with an initial commit. An empty repository cannot be added as a git submodule, which is what you are about to do in a second.

Publishing the repository

Publish the repository to GitHub using the hub command-line tool:

hub create
git push origin master

If you browse to at this point, you will see that the site is already live, using the contents of This is going to be replaced by Hugo-generated content as you finish this walkthrough.

Cleaning up

You can now safely remove your local clone of You won’t need it anymore.

cd ..
rm -rf

Linking the repositories

You are almost done with the repository setup. The final step is to link the repository to the blog repository, by making the former a git submodule of the latter.

Return to the blog repository, and invoke the following commands in its top-level directory:

cd blog
git submodule add \ \
git commit -am "Add submodule public"

The public directory is where Hugo generates the content. Adding the repository as a submodule at this exact location makes it easy to push the generated content to

Continuous Deployment

We can now start to think about Continuous Deployment. Deploying a change such as a new post to the live blog requires several steps:

  1. You push the change to the blog repository.
  2. Hugo is triggered to rebuild the site content.
  3. The content is pushed to the repository.
  4. The repository is deployed to GitHub Pages.

In this section you set up continuous integration on the blog repository to achieve steps 2 and 3, using Travis CI. The last step—deploying from to GitHub Pages—does not require further setup.

Setting up a bot account

Travis CI needs write access to the repository to be able to push the generated content to it. Instead of granting the CI job access to your personal GitHub account, and thus to all of your repositories, you will set up a separate bot account with collaborator access to the repository.

Create a GitHub account named username-blog-bot, replacing username by your GitHub username. This can be done using GitHub’s SignUp page, after logging out of your personal account. The bot account is just a normal GitHub user account.

Note that you need to use a separate email address for the bot account, since GitHub accounts must have unique email addresses. A useful technique in this scenario is subaddressing (also known as plus addressing): Append +blog-bot to the local part of your email address (the part before the @ sign), and mails to that address will be delivered to your normal inbox.

When you’re done setting up the GitHub account, go to the Settings page for the repository, and add the bot account as a collaborator.

Adding GitHub credentials to Travis CI

With the bot account set up, you can add the credentials to Travis CI.

On Travis CI, go to the Settings page of the blog repository.

Add an environment variable named GITHUB_AUTH_SECRET.

Set the value to, using the credentials of the newly created bot account.

Ensure that the Display value in build log switch remains in the off position.

Configuring Travis CI

Travis CI is configured by adding a YAML configuration file named .travis.yml to the top-level directory of the repository.

Continuous integration for the blog repository needs to perform three tasks:

  1. Install Hugo into the CI environment.
  2. Invoke the Hugo command-line tool to rebuild the site.
  3. Deploy the new content to

The third step is delegated to a shell script, which is the subject of the next section.

Create the file .travis.yml with the following contents:

  - curl -LO
  - sudo dpkg -i hugo_0.55.4_Linux-64bit.deb

  - hugo

  - provider: script
    script: ./
    skip_cleanup: true
      branch: master

Note that skip_cleanup: true is required so that Travis does not remove the generated files before running the deployment script.

Adding the deployment script

Create the script in the blog repository, again replacing username with your GitHub username:


echo -e "\033[0;32mDeploying updates to GitHub...\033[0m"

cd public

    touch ~/.git-credentials
    chmod 0600 ~/.git-credentials
    echo $GITHUB_AUTH_SECRET > ~/.git-credentials

    git config credential.helper store
    git config ""
    git config "username-blog-bot"

git add .
git commit -m "Rebuild site"
git push --force origin HEAD:master

You can also invoke this script manually on your machine, after running hugo to rebuild the site. Outside of CI, the script uses your normal GitHub credentials to commit and push the generated content.


Finally, commit the added files and push them to the blog repository.

git add .travis.yml
git commit -am "CI: Build and push to"
git push

You can now visit to see your blog building. When CI has completed, your blog should be live at

Some remarks about the CI setup

Two remarks about the CI setup.

First, note that CI never updates the blog repository to point to the new commit in the submodule. It cannot, because the bot account does not have write access to this repository. This means that the blog repository is left pointing at the initial commit of the submodule.

This is not really an issue, because our site is deployed directly from, rather than from the blog repository’s submodule.

Second, note that the deployment script force-pushes the generated content, effectively replacing HEAD and effacing history. This is no big deal, as the repository only contains generated content.

The reason for force-pushing is somewhat subtle: As mentioned above, the submodule still points to the initial commit in the repository. To perform a normal push you would therefore first need to pull from origin.

Unfortunately, this is impossible because the submodule is checked out in detached HEAD mode and has no information about local and upstream branches. Incidentally, this is also the reason why the last argument to the push command is HEAD:master rather than master.

Writing a post

Blog posts are written using Markdown syntax, with a YAML preamble called front matter.

Invoke the Hugo command-line tool to generate the source file for the new post:

hugo new posts/

The generated file is located at content/posts/ and looks something like this:

title: "My First Post"
date: 2019-04-27T10:48:29+02:00
draft: true

Use your favorite editor to write the actual post, and view your changes locally using Hugo’s built-in server:

hugo server --watch --buildDrafts

Remove the draft line from the front matter when the post is ready to be published.

Commit and push. Your new post should go live once CI has completed.