Reviving my personal site with Zola and Github Pages
2024-10-04
Recently, something in the back of my mind reminded me that I used to have at least a minimal presence on the web beyond my GitHub profile. My personal site has been long dead - it was last seen on the Internet Archive sometime in 2018, and I think it's fair to say that my professional life has had some changes since then. It's been so long that I've sort of forgotten by now, but to the best of my memory, the previous incarnation of this site was hosted on NearlyFreeSpeech. As the archive.org link suggests, though, this was not an especially sophisticated setup, and I don't have any real need to recreate it. I believe GitHub Pages actually did exist at the time, and I'm not really sure why I avoided it for so long - probably because it seemed to easy to be much fun. Anyway, at this point I decided I'd rather go for actually free rather than nearly free, and figure out a way to make my own fun on top of GitHub Pages.
¶ Goals
My motivation here was pretty evenly split between personal and technical reasons. On a personal level, lately I have found myself wanting to put some words out there. I've gotten a lot of help from other folks' writing, and I've also been accumulating my own small list of topics that I think I have something to say about.
On the technical side, I had a few main goals:
- Hands-on simple web design - I basically never get to do any UI/frontend work in my day-to-day, so I wanted to be pretty hands-on with the web design side of things. I don't really want to spend a ton of time learning complex JS frameworks, though, so I wanted to stick to relatively basic HTML/CSS, with minimal Javascript as needed.
- Markdown-based text editing - Markdown has become the de facto standard for technical writing, and it's my personal preference as well. The ecosystem for Markdown editing is great; I've started using Obsidian for personal notes, but there are a few other editors out there that are intriguing. Having the site be Markdown-based will (hopefully) help me write more frequently.
- Easy development and deployment workflow - This one is pretty much a given, but I don't want the development workflow to be too painful. This means good support for both local development and deployment workflows.
¶ Rewrite it in Rust (ish)
Some time in college, I messed around with setting up a little Jekyll-based blog, also hosted on NFSN. I've never really been a big Ruby enjoyer, so Jekyll didn't really hold my interest and I don't
think I ever wrote a single non-test post. I did take a cursory look at the repo, and what I found was not especially inspiring. There's some nasty openssl
stuff in the TravisCI config there, and some sort of ancient version of a GitHub Actions workflow. That stuff is probably better left in the past.
My day jobs over the last few years have had a disappointingly small amount of Rust involved, so I started out by searching for a Rust-based static site generator. The implementation language doesn't really make that much of a difference, but if I end up wanting to dive in and contribute, getting the chance to write some Rust would be nice. Also, I'm very far from a Javascript expert, so I'm happy to start with something that makes very few assumptions about the frontend implementation. I've seen mdBook used for a lot of open-source documentation, but that isn't really the style I was going for. The remaining two mature candidates seem to be Zola and Cobalt. To be honest, I didn't look too closely at Cobalt - one sentence in the Zola README
sold me pretty much immediately:
This tool and its template engine tera were born from an intense dislike of the (insane) Golang template engine and therefore of Hugo that I was using before for 6+ sites.
I have lost enough time fighting with Golang templates to deeply appreciate this sentiment.
Zola also meets all my requirements:
- Its template system is straightforward yet flexible, and it doesn't come with any particular notions about frontend technologies out of the box (aside from SASS support).
- It's Markdown based, with a pretty powerful shortcode system as well.
- Local development is easy with
zola serve
, and there seems to be some existing GHA community support.
¶ Setting up a development workflow
In order to support deploying branches to test changes live on other devices, I decided to set up a two-repo system[1]:
- A development repo, with two primary workflows:
- When a PR is opened, build a copy of the site based on the PR, and push it to the development GitHub Pages deployment.
- When a PR is merged, mirror the commit to the main repo.
- A "prod" repo, with just one workflow:
- On push to
main
, build and deploy to GitHub Pages.
- On push to
The dev PR workflow seems like a natural starting point, so let's build that one first. All it needs to do is:
- Install Zola, so we can build the site.
- Build the site with a custom prefix - we need to configure not only the development Pages deployment root, but also the special PR directory structure.
- Push the built site!
Normally, I prefer to use the GHA-based Pages deployment route. However, the development site might need to support multiple in-flight PRs being published at once. In this case, it makes more sense to just push each PR to its own directory on a shared gh-pages
branch and use the older branch-based publishing workflow. Tying those all together, we get a workflow that looks like the following:
on:
pull_request:
permissions:
contents: write
name: Build and deploy staging site
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.2
- name: Build
run: zola build --base-url https://csssuf.github.io/<repo-scrubbed>/pr-${{ github.event.pull_request.number }}
- name: Archive
uses: actions/upload-artifact@v4
with:
name: pubsite-${{ github.sha }}
path: public
push:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Fetch artifact
uses: actions/download-artifact@v4
with:
name: pubsite-${{ github.sha }}
path: pr-${{ github.event.pull_request.number }}
- name: Commit and push
run: |
git config --global user.name "Github Actions"
git config --global user.email "noreply@csssuf.net"
git add pr-${{ github.event.pull_request.number }}
git commit --allow-empty -m "Add/update PR ${{ github.event.pull_request.number }}"
git push
That should get us what we're looking for. And lo and behold, we have per-PR site builds:
Exactly what we wanted!
¶ Pushing to prod
We can also crib about 90% of this workflow for the main site build. The only differences are that:
- The main site build should just accept the default base URL.
- We don't need to push to a branch for the main site, since we'll use the GHA publishing workflow for Pages. That means we also won't need to split the workflow into two jobs.
And indeed, the main site workflow is very simple:
on:
push:
branches:
- main
permissions:
contents: write
pages: write
id-token: write
name: Build and deploy site
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.2
- name: Build
run: zola build
- name: Archive
uses: actions/upload-pages-artifact@v3
with:
path: public
- name: Deploy
uses: actions/deploy-pages@v4
That part's easy. All we have left to do to connect the two halves is our workflow to mirror commits to main
in the development repo to main
in the primary repo. To do this, we'll need to give the development repo access to push to the primary repo. I've done this with a fine-grained Personal Access Token, which lets us scope the token to just the permissions and repos involved. After creating a token associated with both repositories that has read-only metadata and read/write contents permissions, I stored that token as an Actions secret in the development repo. Then, we can assemble the pieces to check out both repos, copy the contents, and push to the primary repo:
on:
push:
branches:
- main
name: Mirror to main repo
jobs:
push:
runs-on: ubuntu-latest
steps:
- name: Checkout this repo
uses: actions/checkout@v4
with:
path: development
- name: Checkout main repo
uses: actions/checkout@v4
with:
token: ${{ secrets.PUBSITE_REPO_GITHUB_TOKEN }}
repository: csssuf/pubsite
path: main
- name: Copy, commit and push
run: |
cp -rt main development/{config.toml,content,sass,static,templates,themes}
cd main
git config --global user.name "Github Actions"
git config --global user.email "noreply@csssuf.net"
git add .
git commit --allow-empty -m "dev: ${{ github.sha }}"
git push
After merging to main, we can see that the site is pushed to the primary repo and deployed for real - otherwise, you wouldn't be able to read this post!
¶ Clean up after yourself
We're able to test out changes before merging, and deploy the prod site upon merging PRs, but now we have one more problem to solve - all those old PRs are still included in the development site. There's no reason to keep those around, so we should clean them up when we merge a PR to main. Fortunately, GitHub lets us filter pull_request
events by which action triggered them. The one we're interested in here is closed
, aptly enough. Since this is still a pull_request
event, we have the PR number accessible in the event context, which lets us know which directory to clean up. Given that, the corresponding workflow is pretty straightforward:
on:
pull_request:
types:
- closed
permissions:
contents: write
name: Clean up merged PR preview
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
- name: Commit and push
run: |
git config --global user.name "Github Actions"
git config --global user.email "noreply@csssuf.net"
git rm -rf pr-${{ github.event.pull_request.number }}
git commit -m "Clean up PR ${{ github.event.pull_request.number }}"
git push
Just to confirm it's working, let's look at our gh-pages
branch on the development repo:
Looks good! We only see one directory, which corresponds to the currently-open PR.
¶ The future
I didn't go into too much depth on the actual Zola layout and implementation here, but it's honestly not as interesting - it's some pretty basic HTML templating and a tiny bit of CSS. However, it's definitely helped satisfy the urge I had to build something tangible working with technologies I rarely get to touch.
It took me about two months to write this post after doing the initial setup here. I spent some of that time tweaking the look of the site, but if I'm being honest, most if it was spent finding my voice for this post. While I'm still figuring out my writing workflow, I've got a few other ideas on the backburner. Hopefully the motivation to explore them means that it won't be another two months before I write the next post!
-
Using two repos isn't necessarily ideal here, but I wanted to keep the two Pages deployments separate in order to keep the PR builds relatively private. ↩