A 1:1 Site Rebuild
Contents
Since early 2019 I've been using Zola to power my website.
As detailed in "New Year, New Site", the primary impetus for picking Zola was to allow me to focus less on fiddling with my site and more on writing.
In that regard, the choice was quite successful. Once I got it set up the way I wanted, it has largely faded into the background and given me room to focus on writing and publishing content to the site.
Zola has served me well for five years at this point, but recently I have found myself feeling that Zola isn't quite what I want.
The urge
Back in January I sent a Discord DM to my friend Steffen:
I had the very dangerous urge to overhaul my website tech the other day.
My north star in all of this was the idea of using a general-purpose programming language for the templating.
The thing that felt the most restrictive about Zola is having limited programmability inside of the templating engine, Tera. While Tera does allow you to define your own functions and filters when using it directly, since I was consuming Zola as a pre-built binary I could not make any adjustments.
This meant that for simple things like formatting numbers I had to upstream my changes to Zola or Tera in order to make use of them. While this approach is fine for general-purpose features—I have no problem contributing to the ecosystem as a whole—for features more specific to my site this becomes an untenable solution.
Additionally, the mere exercise of writing anything more complex than a loop and a few conditionals inside of a Tera template was feeling painful.
A note on templating languages
Early in my career I was working on an app using AngularJS, which required the use of Angular-specific attributes in order to express things like conditionals:
<div>
<h1 ng-if="isLoggedIn">Welcome back!</h1>
<h1 ng-if="!isLoggedIn">Please sign in.</h1>
</div>
This adds a lot of additional API surface area to learn, as you need to know how different concepts like conditionals or iteration are represented in Angular.
Pardon the lack of syntax highlighting, this was one of the things that didn't make the cut in the rebuild (more on that later).
In 2016 when I first started learning React, a common aphorism used within the community was that "React is just JavaScript". In contrast to AngularJS, React relied heavily on existing JavaScript syntax instead of having React-specific APIs to learn. For instance, conditionals in React components can just be ternary expressions1:
const Greeting = ({ isLoggedIn }) => (
<div>{isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign in.</h1>}</div>
);
In fact, you can even use React without using JSX at all, making it really just plain JavaScript:
const Greeting = ({ isLoggedIn }) =>
React.createElement(
'div',
null,
isLoggedIn
? React.createElement('h1', null, 'Welcome back!')
: React.createElement('h1', null, 'Please sign in.'),
);
I've long-since fallen off the JavaScript bandwagon, but the React mindset has stuck with me since then.
A thought embedded itself in my brain:
What if I could just use Rust as my templating language?
Inspired by working with GPUI at Zed, I decided to experiment with building an eDSL for writing HTML inside of Rust.
I call it Auk:
fn greeting(is_logged_in: bool) -> HtmlElement {
div().child(if is_logged_in {
h1().child("Welcome back!")
} else {
h1().child("Please sign in.")
})
}
My initial explorations were enough to convince me that this was a path I wanted to pursue, and I settled on Auk as my replacement for Tera.
The rebuild begins
When I first begin thinking about doing a site rebuild I had the idea of building a "static site generator in a box". It would come with a bunch of existing components that you might want in a static site generator, but allow customizing or swapping out components to suit your needs.
To this end, I created Razorbill and began using it to rebuild my site.
In the interest of keeping the scope of the rebuild as small as possible, I decided to limit it to just swapping out Zola for Razorbill while leaving everything else as untouched as I could. This meant making Razorbill understand Zola's content structure, frontmatter, and shortcode syntax.
This decision meant I could still continue to write and publish content on my site during the rebuild process.
Replacing the templating system was in-scope, so I duplicated the Tera templates and ported them to Auk. Having to fork the templates like this did mean that any changes to the templates needed to be done twice, so I tried to refrain from making any major changes to them while the rebuild was in-flight.
After a week or so of work, I had a version of my site running on Razorbill that very closely resembled my Zola site:
You may, as I did, look at that and think "so you're done, right?" In reality, there was still a long ways to go.
The long tail migration
After working on the site rebuild for two weeks, progress ground to a halt under the weight of functionality that Razorbill needed to support in order to achieve parity with Zola.
Shaving the yak of static site generator features seemed daunting, and I ended up setting the project aside.
I would not revisit it for almost seven months.
A much-needed catalyst
In late July, weary of having this unfinished project hanging over me, I picked it up again. I had to finish it.
The main impediments thus far were not knowing how close I was to being done, and what things I still needed to implement or fix.
To help improve my understanding of the status of the migration, I created a small tool called site_compare
to build both versions of my site and then compare the differences between them.
The way it worked was rather simple:
- Build both sites
- Format the output files with Prettier
- Diff the content with
similar
- Generate an HTML report
The generated report would give me a high-level overview of which files had differences:
It would also show diffs for each of the changed files:
Building site_compare
ended up being the catalyst I needed to push the rebuild over the finish line.
I quickly settled into a fast-feedback cycle of:
- Make a change in Razorbill
- Run
site_compare
- See what changed in the comparison report
After two weekends of this, I finally reached my goal: a near 1:1 rebuild of my entire website.
Here is the final comparison report:
Syntax highlighting
I ended up cutting syntax highlighting from the scope of this rebuild.
It seemed like a complex enough feature that could make the already-dragging project take even longer. Additionally, I wanted to explore alternative approaches to syntax highlighting to how Zola does it, which felt like a good standalone project for my future self.
Once I decided to cut it, it was easy to set markdown.highlight_code
to false
in my Zola config to remove any differences stemming from syntax highlighting from the site_compare
diffs.
Deploying the new site
Satisfied with the state of the new site, the last thing remaining was to deploy it.
I set up a GitHub Action for building the new site and deploying to Cloudflare Pages. This did require a bit of shuffling things around in Cloudflare, as it turns out there's no way—at least, not that I could find—to disconnect a Cloudflare Pages project from a GitHub repo once connected.
After inspecting a preview deployment and confirming that all was in order, I merged my PR and the new site went live.
Coda
After dragging this rebuild out for more than half the year, I am glad to finally have it done.
While this rebuild has been almost entirely unnoticeable to any lay visitors to my website, it serves as a foundation for all of the things I want to do in the coming months and years.
Even though I built it specifically for my own needs, I decided to open-source site_compare
in its current state in case someone else might benefit from it. You can find it on GitHub.
I used git-filter-repo
to extract it from my website repository:
nix-shell -p git-filter-repo
git filter-repo --subdirectory-filter tools/site_compare --replace-message ~/projects/expressions.txt
With the following contents in the expressions.txt
file:
site_compare: ==>
When I have some time I'd like to clean it up a bit and make it a bit more usable outside of its original use-case.
Since if
is a statement in JavaScript and cannot be used as an expression.