Super Powered Blogging With Hugo and GitHub Pages and Actions
Nowadays blogging platforms are a dime a dozen. You’ve got WordPress that is still king, but it’s kind of bloated and has plenty of weird gotchas that can lead you down into rabbit holes that lead you to realize there are more than 10 levels of abstraction until you get down to calls to cURL from initiating an HTTP GET request with their API (this is a horrific story, I don’t know if I want to think about it any further than that). Sometimes, you want something lean and fast. You’re just writing a simple blog for yourself, you don’t need a fancy WYSIWYG editor and plugins galore. You could go for a headless SaaS option like Contentful, but that’s expensive and even more overkill than WordPress for your needs. Where is something simple?
Static site generators to the rescue! These handy tools are great for building out a simple blog just to share your thoughts and knowledge with the world. They ultimately end up being deployed as static files, but have plenty of flexibility and power to handle whatever your blogging requirements are. Probably. You may need an API for some things, but we can talk about that later. Static sites have another benefit though. They are incredibly cheap to deploy and easy to scale. Sometimes they are even free to deploy, as is the case with GitHub Pages. Coupled with GitHub Actions we’ll get a completely automated pipeline set up so you can write posts on GitHub itself if you wish! Static site generators are a dime a dozen, with choices between Jekyll, Gatsby.js, Middleman, 11ty, or Hugo. GitHub Pages supports Jekyll natively, but it uses Ruby and right now you are developing this on a Windows laptop and you don’t want to deal with that dumpster fire to get an environment stood up locally. So… we’re going to go with Hugo as an example.
What are these things and why do I care?
-
Hugo is a static site generator written in the Go programming language. However, little to no knowledge of Go is necessary to begin using it. Being powered by Go allows it to run easily on Windows, macOS, and Linux with no dependencies. And it’s blazing fast. It’s also particularly featured towards blogging with features like syntax highlighting and Markdown support out of the box.
-
GitHub Pages is a hosting platform for static websites powered by GitHub. It provides free SSL and supports custom domains. Best of all, it is free for all public repositories.
-
GitHub Actions is GitHub’s answer to CI/CD for the GitHub platform and is free for all public repositories. For the purposes of this article, it will be our method of handling automated deployments of our blog to GitHub Pages. CI/CD stands for continuous integration/continuous deployment. CI/CD platforms provide tooling for automating various processes that go into a software deployment pipeline, including testing, linting, and deployment. In our case, we have no logic to test and don’t care about linting, so we are only worried about the automatic deployment of code. There could be potential use cases for automated browser testing depending on how far you take this platform, but that is a topic for another post. :)
Together, these technologies allow you to build a pretty slick blogging set up for vomiting out your thoughts and dreams… for free! Utilizing the power of these tools will allow us to develop some very powerful features into our blog that normally require something more complex like WordPress or another CMS. Hugo alone takes care of things like taxonomies, page/post templating, syntax highlighting, and Markdown support. With GitHub Actions, we will also be able to build in post scheduling and other publication workflows. The sky is the limit here.
So, how do I get started?
To follow along with this article, you will need to have a few things ready to go.
- Create a GitHub account at https://github.com
- Install Git on your machine (if it isn’t already) following the directions at https://git-scm.com/downloads
- Install Hugo following the directions at https://gohugo.io/getting-started/installing/
- Set up a Hugo site. I’ll point to their documentation on how to quickly get a Hugo site set up on your machine: https://gohugo.io/getting-started/quick-start/
Setting up your GitHub Repositories
GitHub Pages is a feature that is enabled at the repository layer. There are three types of GitHub Pages: user, organization, and project. We will be creating project pages in this article. By default, project pages use the gh-pages
branch of the repository as its publishing source, but can be configured to use the master
branch or the /docs
folder of the master
branch. User and organization pages only support the master
branch and /docs
folder options. When GitHub Pages is active, it will serve all of the files in the publishing source of certain MIME types. It will serve images, HTML, CSS, and JavaScript. Which is all we need for a simple blog.
First of all, you’ll need a repository for your Hugo files. We could just use the gh-pages
branch of that same repository to host the built static files, but that requires that repository to be public in order to get GitHub Pages enabled on it for free. That kind of defeats the point of some of Hugo’s features like drafting and post scheduling. Sure, the post may not be on the website, but the content is sitting out in the open on your repository. Maybe you want those drafts to actually be private! To get around this, we will use two repositories. Your main repository can be a private one. Don’t worry, we’ll be needing access to GitHub Actions for this repository which comes with a generous 2,000 minutes per month allotment for private repositories on Free tier GitHub accounts. Public repositories have access to GitHub Actions completely for free. It’s up to you!
The second repository has to be public for free GitHub Pages. Private repositories do not get access to GitHub Pages until you are at least on the Pro plan (which is $4/mo). This repository will essentially act as our build target.
For the purposes of demonstration, I will use https://github.com/cameronbroe/hugo-gh-actions-main as my main repository and https://github.com/cameronbroe/hugo-gh-actions-built as my build repository. The main repository is taken from the Hugo quick start example and the built repository is the GitHub Pages host.
So now you’ve got two repositories? Great, let’s go on and initialize our build repository. We will need it to have something in it in order to link it to our main repository.
These commands will create a repository, add an empty README file, and push it to the GitHub build repository with a gh-pages
branch.
mkdir hugo-gh-actions-built
cd hugo-gh-actions-built
git init
git checkout -b gh-pages
touch README
git add README
git commit -m "init build repo"
git remote add origin https://github.com/cameronbroe/hugo-gh-actions-built
git push -u origin gh-pages
Linking these two repositories together
We will make use of a neat feature of Git called submodules. Basically, a submodule allows you to specify that a directory of a Git repository should actually be another Git repository. This submodule is completely separate, but is contained in-tree with the parent Git repository. By default, Hugo builds its output to the public/
directory. So, we’ll take advantage of this.
The command to do this is as follows:
git submodule add https://github.com/cameronbroe/hugo-gh-actions-built public
IMPORTANT: Use an HTTPS link, not an SSH link. This will simplify handling the submodule inside of GitHub Actions.
Setting up GitHub Actions for deployment
GitHub Actions lets you execute workflows when designated events happen with your repository. This can be any event on GitHub such as a push event, a pull request is opened, or an issue is closed. You can also execute these workflows as a cronjob as well. A workflow is simply a definition of a set of actions that should run on one of their virtual machines. They provide Linux, macOS, and Windows runners currently. These actions can basically be anything you want. We will be using a GitHub Action to run a build on our Hugo repository. These workflows are defined in YAML files in the .github/workflows/
directory of your repository. Let’s create one now for our Build & Deploy workflow.
Create a file at .github/workflows/main.yml
.
Go on and paste the following block in as the contents of the file:
|
|
Whoa…there’s a decent bit there. Let’s go over it in chunks.
Configuring the GitHub Action workflow
First, we need to specify what we want to call our workflow and when we want it to be executed. There are numerous events that you can have a workflow execute from, but for now we’ll just start with every push to the master
branch. This will cause the workflow to trigger everytime we push our code to the repository.
|
|
Next, we need to specify a job to execute as part of the workflow. Multiple of these can be defined, but we only need one for now. We also will go on and specify which virtual environment we want to use. For our purposes, ubuntu-latest
is a great option.
|
|
Getting the repository onto your runner
Then, we start to define what steps we want to do in our workflow. Our first step will be to utilize an existing action, actions/checkout@v2, to actually pull down the repo. We also pass in the argument submodules: true
to the checkout action to tell it to pull down our deployment target submodule. By default, the Git client invoked by the checkouts action is authenticated with your account only with scope for the repository running the action. Since we will ultimately be performing write actions to another repository, you will need to specify an alternative Personal Access Token to use authenticate the Git client with. The Git client is not cleaned up until the end of the job, so all further commands will be authenticated with this Personal Access Token. GitHub Actions supports injecting encrypted secrets into your workflows. In order to set this up refer to their documentation here: https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets. I have called mine AUTHENTICATED_PAT
.
|
|
Because of the way the actions/checkout
action handles the clone and checkout, the repository and its submodules are left on a detached HEAD. It will only grab the latest commit of the primary branch by default. This is fine for our purpose, but the state of the repository is not acceptable for being committed to and pushed, which we will need to do for our deployment target. In our case, the primary branch is going to be gh-pages
.
So, our next step will be to move into our deployment target’s repository, and run a git checkout
to put it on the gh-pages
branch. This will correct the detached HEAD and put the repository is a committable and pushable state. We also will need to configure a name and email with the git
CLI as that is a requirement for committing (so it knows who to tag the commit is from).
|
|
After that, we need to download the Hugo toolchain so we can run our build. The project provides releases through GitHub Releases as tarballs for Windows, Mac, and Linux. We have chosen to use Ubuntu, so we will need to grab the Linux tarball. We can use wget
to download it and then extract the tarball in the same directory.
Downloading and running Hugo
|
|
One of my favorite things about the Go programming language is that the provided toolchain compiles to single, statically-linked binaries by default. That means a Go program can be distributed everywhere by just giving people the right binary for their platform. No need to install any runtime dependencies or libraries. No convoluted set-up processes. Just a binary you can run. So, we don’t need to worry about installing any dependencies or anything before running the Hugo toolchain. By default, when you run the hugo
command, it will build your website into the public/
directory of your site. Since that is what we ultimately want, we can just simply call the Hugo binary we downloaded.
|
|
But what about something other than Hugo?
Technically, this pipeline could be used for something other than Hugo. I just like Hugo because it’s small, simple, and fast. You could use whatever static site generator you want. To do that, you would need to adjust this workflow to instead install your static site generator and run the necessary build command. The virtual machines come with several environments and tools pre-installed, so you may be an npm install
or gem install
away from using your static site generator of choice.
Deploying the built website
Now, we will handle our deployment. All GitHub Pages needs is for a push to the set branch on the repository that has it enabled. So we can just move into our public/
directory, and commit and push. We can prevent failing builds on commits that have no changes to the site by using an OR operator on the commit
to ensure the line does not error out and throw a non-zero status code (which a git commit
command on an unchanged repository will do).
|
|
Seeing it in action
One last change before pushing this up. Hugo needs to have a base URL for the website defined in its configuration in order to generate links correctly. For our examples, we will be using a baseURL
value of https://[your-github-username].github.io/[your-build-repository-name]/
, because we are working with project GitHub Pages. If you plan to use a custom domain, then use that domain for the baseURL
value.
This can be set by putting the following line into your config.toml
baseURL = "https://[your-github-username].github.io/[your-build-repository-name]/"
Go on and commit that workflow file and the configuration change to your main repository and push it up.
git add .github/workflows/main.yml
git commit -m "Adding GitHub Actions workflow for build"
git push -u origin master
GitHub Actions should pick it up automatically and execute your build. In my testing, builds take only about 20-30s to complete, so your website should be up within a minute or two.
Now, you should have a built blog deployed to GitHub Pages with SSL. It can be accessed at https://[your-github-username].github.io/[your-build-repository-name]
. You can see my example here: https://cameronbroe.github.io/hugo-gh-actions-built/
Taking it a step further
Hugo supports specifying a publishDate
in the frontmatter, where a publishDate
in the future from the time of the build will not be part of the final build. This is their method of supporting scheduled posts. We can leverage our build pipeline we have to trigger builds every day, which will automatically publish any scheduled posts! It’s not extremely precise (as GH Actions can only run a build every 5 minutes), but it should do for a lone blogger like yourself. I would suggest running builds at most once an hour and only publish on the hour, as this will keep GitHub Actions minutes usage (remember a private repository only gets 2,000 for free) in check. So, how do we tell GitHub that we want our workflow executed once every hour?
Let’s go back to our events that we have defined we want this workflow to execute on.
|
|
GitHub supports a large number of different events that can trigger a workflow. Convienently, they have a schedule
event! And it simply takes a POSIX crontab entry for input.
So, change your on
object to include a schedule
event with a crontab to execute every hour:
|
|
Note that the crontab is in quotes, YAML considers an asterisk to be a special character which requires them to be in quotes.
And to recap what we have just done
We have looked at what GitHub Pages is and how to deploy a website using the platform. We have leveraged the power of Git submodules to separate unpublished content from published content, providing a sort of rudimentary environment separation to keep your drafts separate from your public content. And we have discussed how to utilize GitHub Actions to automate builds and deployments. This enables a flexible writing environment where you can publish articles from anywhere on any device. Whether you want to write using GitHub’s built-in web editor, a web-based IDE like GitHub Codespaces, or your editor of choice in a more standard environment, you have the ability to publish your content quickly and easily, with support for scheduled publication dates. And the best part is none of this costs a dime. Now go write something for us to read!
Also, big shout to my friends Daniel and Harold for some great edits and feedback on this post before publication. This would have been a mess without your help. :D