In the welcome post of this blog, one of the things I said I wanted to write was a description of how this blog was built. I didn’t want to do that at the very beginning, as it would be weird for a brand-new blog to only have such meta-content.

But the blog has been around for a year now, and I was prompted by something I saw on the Internet:

This tweet is a reference to this Lobste.rs thread, which I found later. And this blog is a static site generated with Hugo, so I figured it was a good opportunity to write something.

Hugo

This blog is a static website generated with the Hugo static website generator. A static web page is a web page which does not change every time you load it; this only exists as a term because many web pages these days are dynamic and have some content that changes when you load it. Static web pages used to be the norm on the Internet, but they are somewhat less common these days. Static websites are great as they can be served by any standard web server and are typically very amenable to caching as the content is not expected to change frequently.

Hugo is a tool to help build full websites that consist of static pages. It’s written in Go, which makes it fairly easy to get started; Go programs are typically (mostly) statically-linked, and can be run on computers that don’t have a Go toolchain installed. It’s also reasonably fast and has a built-in self-updating preview to make it more convenient when you’re writing.

Hugo is pretty flexible; it works for blogs like this one and also other kinds of websites like product pages or personal home pages. There’s a large theme library, and you can build your own when you want something custom.

Hugo does have its detractors (as you can see from the tweet above), and I have complaints about it too. The template language isn’t very simple (it’s based on Go’s template language), but has a lot of custom extensions that are not super easy to discover. (For example, the tweet above was embedded with a “shortcode” that I only discovered from reading a bug report).

The other big complaint that I agree with is the frequency of breaking changes. Prompted by that tweet and Lobste.rs thread, I wanted to update my site. I quickly ran into a breaking change. Fortunately, the theme I use was fixed by someone else on the Internet, so I was able to fix it by updating the theme.

[Update after publishing: I discovered another breaking change in Hugo v0.55.0 that broke the images embedded in my Planning Go Northwest post. To fix it, I had to change my shortcode delimiters from % to < and >.]

Even with these problems, I don’t currently plan to migrate away from Hugo.

Theme

I use the hugo-nuo theme with a few changes. It’s really nice to be able to take a nice theme and make a few changes to it so it’s yours. I was able to contribute one of my changes back so that others can benefit too.

These are the changes I made:

Git & GitHub

I use Git to provide version-control for my blog source, including the text that makes up these posts. I push those to a private GitHub repository. I use GitHub for the majority of my git hosting, and it was convenient to do it in the same place. I do keep this blog private because it holds drafts that I’m not ready to publish yet. I’ve configured this repository so that when I push source changes to it, automation runs Hugo to generate the static pages and publishes them automatically.

GitHub supports Multi-Factor Authentication (MFA), which allows me to require both a password and an additional piece of data (like a TOTP code on my phone or a WebAuthn flow with a Yubikey). This increases the safety of my GitHub account.

Submodules

Hugo expects to find the theme source code in the same directory tree as the site source. The theme source is also tracked in Git and hosted on GitHub, you can see from the links above and you can look at my fork to find all the changes I’ve made.

I use submodules to embed the theme repository inside my site repository. This helps me ensure that the source I use for the theme is consistent with my site. I also track my fork in the submodule instead of the upstream repository so that I can use my changes.

AWS CodeBuild

AWS CodeBuild is a service for running software builds. It’s a good fit for running Hugo as well. I use CodeBuild for a few reasons, both technical and personal.

On the technical side, CodeBuild is easy to integrate with GitHub for receiving push events; this is how CodeBuild knows to rebuild my site when I push changes to my repository. It’s also well-integrated with the rest of AWS, which I use for hosting my site. Part of this integration is its use of IAM roles, meaning that I don’t have any long-term credentials to store in another system and worry about. I can also apply policies to the role to limit the actions CodeBuild can take on my behalf.

On the personal side, I work for AWS and like to use AWS services for my own infrastructure. I also am fairly familiar with how CodeBuild is implemented and have somewhat regular contact with the team; I helped early on in their security review and discovered a fun bug before the launched.

Docker

CodeBuild runs builds inside Docker containers, and you can supply your own image to use. I built my own image with Hugo and another tool inside and made CodeBuild use that image. My Dockerfile looks roughly like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
FROM golang:1.13 as builder
RUN mkdir /apps \
 && cd /apps \
 && git clone https://github.com/bep/s3deploy.git \
 && git clone https://github.com/gohugoio/hugo.git
RUN cd /apps/s3deploy \
 && CGO_ENABLED=0 go build -v -a -installsuffix cgo -o s3deploy
RUN cd /apps/hugo \
 && go build -v -tags extended -o hugo

FROM debian:buster-slim
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
    ca-certificates \
 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /apps/s3deploy/s3deploy /usr/bin/s3deploy
COPY --from=builder /apps/hugo/hugo /usr/bin/hugo

This Dockerfile defines a multi-stage build. Multi-stage builds are useful for separating build toolchains from a deployed image. My first stage uses a Go toolchain to compile Hugo and another tool (s3deploy, covered later), then copies those tools into another image that doesn’t have the Go toolchain inside of it. Hugo is compiled with the extended tag so that it includes Sass/SCSS support, which I use. This extended build links against some C code, which makes it a bit harder to statically link; I build it dynamically-linked against glibc and use a Debian container with glibc installed for actually running Hugo.

Buildspec

CodeBuild uses a Buildspec to define the steps that make up a build. Here’s roughly what my buildspec looks like:

1
2
3
4
5
6
7
8
version: 0.2
phases:
  build:
    commands:
      - hugo
  post_build:
    commands:
      - s3deploy -bucket $BUCKET -path blog -region $AWS_REGION -public-access -distribution-id $DISTRIBUTION -source public -v

This lets me invoke both Hugo and s3deploy whenever I run a build.

Amazon S3

Amazon S3 is a blob storage service provided by AWS. It’s really convenient for storing files; you only pay for the storage space and data transfer that you use. S3 has a built-in website endpoint that’s lets it function as a basic webserver, which is really useful for hosting static websites. The website endpoint is slightly different from the normal S3 HTTP endpoint, as the website endpoint can be configured with things like custom 404 pages and redirections. This site is hosted here.

Amazon CloudFront

Amazon CloudFront is a content-delivery network (CDN) provided by AWS. The typical use-case for a CDN is to cache web content closer (geographically or network-topologically) to the end-user in order to increase speed. This will be really useful if my blog ever gets super popular, but really the only reason I’m using it is to provide HTTPS. Modern websites should be provided over HTTPS as a best-practice, and unfortunately the S3 website endpoint only provides HTTP. I use CloudFront in front of the S3 website endpoint just for this purpose.

Amazon Certificate Manager

Part of serving a website over HTTPS involves having a valid certificate. I use Amazon Certificate Manager (ACM) to generate my certificate; it is free and integrates with CloudFront automatically. If I were hosting this website somewhere other than AWS, I would probably use Let’s Encrypt instead, but ACM is easier to use with CloudFront.

s3deploy

Once the static files for my website are generated by Hugo inside CodeBuild, they need a way to get uploaded to S3 and something has to tell CloudFront to expire its cache. I use s3deploy for this as it was designed for this purpose; it syncs the content quickly by minimizing which files to upload and has an algorithm for determining the best set of paths to invalidate in CloudFront.

Comments

Comments are one of the harder things to enable with a static website, as they’re made up of content submitted by other people. The most common approach to providing a commenting experience on a static website is to load them with JavaScript from somewhere else. There are a number of different platforms to do this. I’ve picked Disqus for now, which is free, but unfortunately has a privacy policy I don’t like. I may change this in the future.

Putting it all together

If you’ve read this far in a post without images, maybe you’d like to see a diagram. Here’s my attempt at visualizing what this all looks like:

Diagram illustrating how the components described in this blog are connected

Other static websites

This whole stack isn’t just for this website, and isn’t unique to blogs. I use the same set of software for my personal homepage, and you can find the source for that website here.

I hope this post helped you understand the pieces involved in building this blog, and possibly gave you ideas for how you can set up your own!