How I built this blog (with Zola)

Table of contents
  1. Zola
  2. Night mode with modern CSS and a bit of JS
  3. AWS & deployment with GitHub Actions
  4. Finishing up

Hello, world! As you might be able to tell, this is my brand new blog. I am planning to occasionally write articles about various technical subjects, and publish some of my older writings to a potentially larger audience.

To begin, I am continuing an established tradition of dedicating the first post to rambling about how I made the blog itself. The technology stack powering this website is not very complicated, but I think there are some interesting aspects to it. I am relatively familiar with many of the technologies, but there were also some new acquaintances.

Zola

The HTML you’re reading right now was generated using Zola, a static site generator written in Rust. This was my first time using it, and so far I am pretty impressed. In my mind there are two notable benefits over other static site generators I have used in the past:

  1. It’s distributed as a single statically linked binary
  2. It’s really fast (at least so far)

The joy of static linking

All the other static site generators I have personal experience with were implemented in interpreted languages — either JavaScript, Python or Ruby. That usually means using any of those generators requires installing specific versions of an interpreter and a package manager, and somewhere between a dozen and a few thousand package dependencies. This complicates both development and deployment to some extent, and oftentimes makes the setup feel somewhat fragile. It doesn’t help that from my experience all three language ecosystems also have some issues with package management and / or cross platform compatibility (by which I primarily mean Windows support). I have been using Node.JS and NPM / Yarn almost daily since 2015, yet I still have to run rm -rf node_modules && npm install at least twice a month to fix some mysterious issue for which there is usually no logical explanation.

Which is why Zola being a self contained executable feels like a breath of fresh air — you avoid all of the aforementioned reliability issues and complexities just by using a different distribution model. I can install it in my CI pipeline just by downloading a binary, and it is practically guaranteed to work. Zola is of course not the first static site generator to do this, but it is the first one that I have used.

Performance and developer experience

The performance of Zola has so far been solid; building this website from scratch with zola build takes under 150 milliseconds, and incremental builds (changing just a markdown file) take about 50 ms. The development server (zola serve) starts in under 100 milliseconds. For an entirely unfair comparison, starting the development server for the Gatsby-powered OpenSAGE website takes about 10 seconds, and incremental builds vary from 100 ms to 300 ms. The production build takes a really long time — about 130 seconds — but this is where you can’t really compare the two tools fairly. The OpenSAGE website has quite a few images and generating thumbnails for those images is Gatsby’s responsibility, which takes the majority of the build time. However, those thumbnails seem to be generated sequentially using just one thread, so there’s some room for improvement there.

There is more to a smooth developer experience than just compilation time. What ultimately matters to me is that the changes made in my editor get quickly and reliably reflected in the browser. Zola is fast at incremental builds, but it takes about a second for the development server to notice when a file changes, which means that the real-world edit latency is slightly worse than some other static site generators. Looking at the source code, the delay seems to be directly caused by the way the file watcher is created. This is really easy to fix. On the reliability front, Zola is solid. The biggest bug I encountered is that creating an empty markdown file crashes the development server. Luckily it has already been fixed for the next release.

Aside from performance, how is Zola as a static site generator? It’s fine. The templating language Tera is simple and easy to use for a site like this. Zola has a comprehensive feature set, and the documentation covers it well, though it occasionally feels like it has been scattered on too many pages. I would maybe like some more high level documentation which would summarise all of the terminology: content, section, page, template and so on. I still don’t have clear mental model of what all of those terms mean.

To be honest, as a spoilt youngster I nowadays prefer a React-style component-based approach for writing web applications and even for relatively simple HTML templating. But I will also be the first to admit that for a really simple site like this the massive complexity of a React + Webpack + Babel setup just doesn’t really make much sense. It would be interesting to try a templating system with the development model of React and Styled Components, but without the inevitable and all-consuming ecosystem complexity of JS. If such templating language already exists, let me know.

Night mode with modern CSS and a bit of JS

Speaking of JavaScript, this site uses almost none of it. The only exception is the night mode toggle button in the upper right corner. Your preferred colour scheme is automatically detected using prefers-color-scheme CSS media feature and window.matchMedia() in supported browsers. The theme is changed from day to night by applying a night class name to the <body> element. When a user changes the scheme, the class name is toggled accordingly and the preference is saved to local storage. If I really wanted I could probably implement the toggle using no JS at all with a classic checkbox hack, but I feel like it would be at the cost of worse user experience and ultimately higher implementation complexity.

The two colour schemes are implemented using CSS custom properties (or CSS variables, as they are oftentimes called). There are two properties which control the primary colours of the website, uncreatively named --primary-bg and --primary-text. Their value depends on the presence of the night class. This was my first time using CSS custom properties, and I now understand what they can offer over pre-processor variables. Here’s the essential SCSS code powering the feature:

:root {
  --day-bg: white;
  --day-text: black;
  --night-bg: rgb(30, 30, 30);
  --night-text: rgb(236, 236, 236);
}

body {
  --primary-bg: var(--day-bg);
  --primary-text: var(--day-text);

  &.night {
    --primary-bg: var(--night-bg);
    --primary-text: var(--night-text);
  }

  background-color: var(--primary-bg);
  color: var(--primary-text);
  // The variables are also used in other elements
}

The layout uses a combination of CSS grid and flexbox, and a handful of media queries to improve responsiveness on narrow resolutions. Since there’s no UI designer sitting next to me telling me not to add them, there are some tacky hover effects and other opulent visual flourishes built with CSS transitions and keyframe animations. Depending on your opinion they either make the site look polished or amateurish — I don’t disagree with either interpretation.

AWS & deployment with GitHub Actions

With the site assembled, the next step is to host it somewhere. I used a familiar AWS stack: S3 static site hosting, CloudFront as a CDN, and Route 53 to tie it all together. The only speed bump on the way (which I have probably encountered every single time when using S3) was the requirement to add a bucket policy to make static site hosting actually work.

The source of the blog is hosted in a private GitHub repository, which is why using GitHub Actions for building and deploying the site was my first choice. Setting up automatic deployments was quite easy. Whenever I push to the public branch Actions checks out my code, downloads Zola, builds the site and deploys it S3 using the AWS CLI. Finally, the script creates a CloudFront invalidation to refresh all cached files.

Screenshot of an example deployment

The entire process takes about 10 seconds from start to finish. To continue the earlier unfair comparison, the OpenSAGE website deployment workflow takes about 5 minutes, where half of the time is spent installing NPM dependencies. Here’s my Actions workflow YAML:

name: Blog deployer

on:
  push:
    branches:
      - public

jobs:
  deploy-blog:
    name: Deploy blog to S3
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Install Zola
        run: |
          wget https://github.com/getzola/zola/releases/download/v0.13.0/zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz
          tar -xzf zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz
      - name: Build site
        run: ./zola build
      - name: Deploy site to S3
        run: |
          aws s3 sync ./public s3://blog.paavo.me
          aws cloudfront create-invalidation --distribution-id <my distribution id> --paths "/*"
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}

Finishing up

And that’s it for the first post of this blog! I hope you tolerated reading my scattered thoughts about package management, static site generators, developer experience, modern CSS, AWS and $GPT3_INPUT_6.