Static site with Hugo and GitHub Pages

Cover image

An exploration of Hugo, a static site generator, and how to host the resulting static website on GitHub pages.

As of 2020, I have switched from Hugo to Gridsome. Gridsome's main benefit is that it is just VueJS when you need to do something a bit advanced.

Why Hugo?

Benefits of a static site:

  • cheap to host
  • good security (no server-side interpreter)
  • good performance

Benefits of Hugo (e.g. compared to Jekyll or Hexo):

  • single binary
  • fast building time

Hugo Quick Start

We will follow the steps in Hugo's Quick Start guide.

Step 1: Install Hugo

Go get the latest binary from https://github.com/gohugoio/hugo/releases. Remark: there is an "extended" version with native support for SASS/SCSS & PostCSS.

Step 2: Create a New Site

$ ./hugo new site matrey
$ cd matrey

Let's check the directory structure that was generated:

$ ls -1 --group-directories-first
archetypes
content
data
layouts
resources
static
themes
config.toml

Mainly:

  • create your pages under content
  • put assets (JS, images...) under static
  • override theme layouts under layouts

By default only 2 files exist:

$ find . -type f
./config.toml
./archetypes/default.md

$ cat ./archetypes/default.md 
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

$ cat ./config.toml
baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"

Let's push that to the git repository holding the source:

$ git init
$ git remote add origin git@github.com:matrey/matrey.github.io-source.git
$ git add .
$ git commit -m "./hugo new site"
$ git push -u origin master

Step 3: Add a Theme

$ git submodule add https://github.com/nanxiaobei/hugo-paper.git themes/hugo-paper
$ echo 'theme = "hugo-paper"' >> config.toml
$ git add config.toml
$ git commit -m "Add hugo-paper theme"

Importing the theme as a git submodule means we won't be able to edit its source code as part of our codebase. But that's alright, because the proper way to overload a theme is by leveraging Hugo's lookup order.

For instance for a layout change:

Let’s say you really like theme foo, but you wish the homepage was different. All you have to do is copy the homepage template:

your-site/themes/foo/layouts/index.html

And paste it under your own layouts folder:

your-site/layouts/index.html

Step 4: Add Some Content

$ ../hugo new articles/static-site-hugo-github-pages.md

You might want to get a refresher course on Markdown from this Markdown Cheatsheet.

Note that you can explicitly add a summary divider in your page content, to control what content is shown on pagelists. According to Manual Summary Splitting:

[...] you may add the <!--more--> summary divider where you want to split the article.

Content that comes before the summary divider will be used as that content’s summary and stored in the .Summary page variable with all HTML formatting intact.

Be careful to enter <!--more--> exactly; i.e., all lowercase and with no whitespace.

Step 5: Start the Hugo server

By default Hugo will not publish content in "draft" status

Start the Hugo server for local development, with drafts:

../hugo server -D

If you are not in the root folder from the Hugo site, you can pass its path through -s ~/my-hugo-site

Then navigate to your new site at http://localhost:1313/

Make Hugo deployment-ready

Let's change the copyright holder in the footer. Unfortunately it is hardcoded to .Site.Title in the template, so we will have to overload it:

$ mkdir -p ./layouts/partials/
$ cp ./themes/hugo-paper/layouts/partials/footer.html ./layouts/partials/

Note that the Site Variables page mentions .Site.Author as a variable that should be available by default. When using it in the layout, it's true there is no error (so, the variable seems to exist), but it ends up reading © 2019 map[]. It's really unclear what is the status on this feature. So we'll use a distinct variable.

Let's create a new variable in config.toml by adding the following:

[params]
  AuthorName = "Mathieu Rey"

Then edit our copy of the footer to use .Site.Params.AuthorName:

$ diff ./themes/hugo-paper/layouts/partials/footer.html ./layouts/partials/footer.html
3c3
<   <span>&copy; {{ now.Year }} <a href="{{ "" | absURL }}">{{ .Site.Title }}</a></span>
---
>   <span>&copy; {{ now.Year }} <a href="{{ "" | absURL }}">{{ .Site.Params.AuthorName }}</a></span>

Publication folder

By default the publication folder is named public. Let's set a more explicit one in config.toml:

publishDir = "matrey.github.io"

Also, don't forget to replace the default baseURL with the real production URL (including a trailing slash):

baseURL = "https://matrey.github.io/"

Let's publish the site by calling hugo without any parameter:

$ ../hugo

$ find matrey.github.io/
matrey.github.io/
matrey.github.io/tags
matrey.github.io/tags/index.html
matrey.github.io/tags/index.xml
matrey.github.io/tags/page
matrey.github.io/tags/page/1
matrey.github.io/tags/page/1/index.html
matrey.github.io/sitemap.xml
matrey.github.io/index.html
matrey.github.io/index.xml
matrey.github.io/js
matrey.github.io/js/instantclick.min.js
matrey.github.io/js/highlight.min.js
matrey.github.io/css
matrey.github.io/css/github-gist.min.css
matrey.github.io/css/style.css
matrey.github.io/img
matrey.github.io/img/favicon.ico
matrey.github.io/img/apple-touch-icon.png
matrey.github.io/page
matrey.github.io/page/1
matrey.github.io/page/1/index.html
matrey.github.io/categories
matrey.github.io/categories/index.html
matrey.github.io/categories/index.xml
matrey.github.io/categories/page
matrey.github.io/categories/page/1
matrey.github.io/categories/page/1/index.html
matrey.github.io/404.html

Note that Hugo does not remove generated files before building. It might be a good idea to purge the folder's contents before each generation.

Let's also exclude the publication folder from the main git repo, as it will have its own repo:

echo '/matrey.github.io/**' >> .gitignore

Note that we could have made this a git submodule, like for the theme. But we are not interested in tracking changes in this repo (as it can be re-generated from the source in the main repo). Using a submodule would quickly bother us with this kind of display:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   matrey.github.io (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

GitHub pages

Create the repository

For a user site, you just need to create a new repository named username.github.io, where username is your username on GitHub.

Let's push the contents of our publication folder to it:

$ cd matrey.github.io/
$ git init .
$ git add .
$ git commit -m "Hello world"
$ git remote add origin git@github.com:matrey/matrey.github.io.git
$ git push -u origin master

Note that changes might not appear immediately, due to CDN caching on GitHub side.

Custom 404 page

You just need to have a file named 404.html at the root of your site. It turns out Hugo takes care of it natively:

When using Hugo with GitHub Pages, you can provide your own template for a custom 404 error page by creating a 404.html template file in your /layouts folder. When Hugo generates your site, the 404.html file will be placed in the root.

The theme we picked already provides a basic version:

$ cat themes/hugo-paper/layouts/404.html 
{{ partial "header.html" . }}
<div class="not-found">404</div>
{{ partial "footer.html" . }}

Scripting the publication

What we want:

  • clear the publication folder to make sure we remove content that should not be there anymore
  • run the Hugo compilation
  • commit all files
  • push to GitHub to update the website

Let's adapt the deploy.sh script from Hugo's documentation:

#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Adapted from https://gohugo.io/hosting-and-deployment/hosting-on-github/#put-it-into-a-script

# /!\ Paths to edit /!\
HUGO_BIN=../hugo
PUBLICATION_DIR=matrey.github.io

# If a command fails then the deploy stops
set -e

# Clear the publication folder (except the .git folder)
# Thanks https://stackoverflow.com/a/22347541/8046487
cd "${DIR}/${PUBLICATION_DIR}"
printf "\033[0;32mCleanup target folder...\033[0m\n"
git rm -rf . # removes all versioned files
git clean -fxd # removes any file not under version control

# Build the project
cd "${DIR}"
printf "\033[0;32mHugo build...\033[0m\n"
"${HUGO_BIN}"

# Add changes to git
cd "${DIR}/${PUBLICATION_DIR}"
git add .

# Commit changes
printf "\033[0;32mCommit to git...\033[0m\n"
msg="Hugo rebuild on $(date)"
if [ -n "$*" ]; then
  msg="$*"
fi
git commit -m "$msg"

# Push to GitHub
printf "\033[0;32mPush to GitHub...\033[0m\n"
git push origin master

exit 0

Note that this only takes care of the published version. Don't forget to also commit your changes in the source repository.