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:
@hillelogram Have you considered writing your own static site generator? Re: thread.
— Dan Luu (@danluu) March 28, 2020
I'm stuck on Hugo 0.19 after having to unbreak my site the last three times I updated Hugo. In the time I spent fixing Octopress and then Hugo, I think I could have written my own site generator pic.twitter.com/x0FfWHqapV
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:
- Use FontAwesome instead of iconfont – Both iconfont and FontAwesome provide icon/logo fonts for the Internet. This theme uses the icons and logos in a few places, including links to social-networking sites. Iconfont is a provider from China, and unfortunately I’m unable to read much of their site as I don’t read Chinese. FontAwesome’s site is in English, and I can both find and understand their license. Because of this, I wanted my site to use FontAwesome instead of iconfont.
- Skip the @ symbol on post author – The theme prepends an @ to the author’s name, but I didn’t want that on my site. I haven’t contributed this upstream yet; it’s not clear whether there would be any interest.
- Use
.Permalink
instead of.RelPermalink
for some assets – I wasn’t able to trace what the problem was, but I suspect it’s because I useblog/
as a path prefix for my site. I won’t contribute this upstream until I understand what’s causing the problem. For now, I just fixed it for my site. - Customized the rotation for my avatar – When I first started using this
theme, the avatar rotated 180 degrees when you hovered over it. This was
updated later
to rotate 360, but I prefer the whimsy of having my picture upside-down. I
was able to override this in the source tree for my site directly, so there’s
no change to the theme itself. In my site source tree, I have a file named
assets/styles/custom.scss
that includes these lines:This overrides the styling for rotation.1 2 3 4 5 6
.avatar { &:hover { // It's more fun to rotate 180 instead of 360 transform: rotate(180deg); } }
- Ensured my avatar wasn’t washed out – An upstream
change turned the avatar into a
link. Unfortunately, the
<a>
tag has styling that reduces its opacity, making my avatar look washed-out. I was able to override this with theassets/styles/custom.scss
file as well:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// The theme makes the avatar have reduced opacity because it's inside <a>. // Override that, then set the other elements of the sidebar to have the // reduced opacity .site-header { & a { opacity: 1; } } .site-menu { & a { opacity: 0.5; } } .social-menu { & a { opacity: 0.5; } }
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:
|
|
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:
|
|
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:
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!