11/5/2025
Rebuilding the site with Astro
Navigate to #Rebuilding to skip the recipe website blogspam and go straight to the implementation details.
It’s been almost nine years since I staked my claim to a little slice of the web,
publishing my first blog post to the long-since abandoned wbadart.info. In the time
since, I’ve developed my writing style, migrated domains, shaved countless yaks
exploring different SSGs, and (this still shocks me sometimes), become a professional
software engineer.
As you can see from the timestamps on my list of posts, my little slice of the web fell quiet for quite some time, owing to the growing responsibilities from a (fortunately) advancing career. Now, at the outset of a semi-planned sabbatical, I’m excited to return to the writing hobby that was once such a great source of joy and learning for me.
The first step was to reclaim the domain: for the last couple of years, I’d had Cloudflare Rules redirecting all traffic to the domain to my LinkedIn profile. As of yesterday, I’ve dropped those rules and my domain now represents my own site once again.
The next step was to delve through my archives to find and consolidate any and all material worth republishing. I found no fewer than five different repos across GitHub, GitLab, and Codeberg (I’m probably missing some) each with material that I’d published in the past, and each with its own interesting build system experiments. What can I say, you can shave a lot of yaks in nine years.
With an initial tranche of about 150 files, I was ready to unify the build systems.
Which build system?
Like any professional developer would, I decided that instead of integrating and maintaining the legacy systems, I would rebuild from scratch in order to incorporate the latest technology trends.
My search parameters:
- Markdown authorship
- No client-side JavaScript in the default output
- File-based routing
- Decent image optimization story
- First-class SSG, but ideally supporting an “islands” concept should I need it
This helped me narrow my list down to React Router 7, Soupault, 11ty, and Astro.
React Router 7

At a previous job, our frontend ecosystem revolved around React Router, but we were stuck on v6, unable to access the amazing server-side features and DX improvements the project had inherited from Remix in v7. Like being trapped in prison, staring longingly through the window bars at the outside world; sunny, free, and beyond my grasp.
So naturally I considered that this could me my chance to join in the fun.
React Router 7 supports file-based routing, MDX authorship (via a plugin), and static prerendering (aka SSG). Alone it doesn’t have any image optimization facilities, but that doesn’t really belong in a routing framework—that’s probably more of a Vite plugin.
Ultimately I decided to keep looking since it seemed that dynamic applications with client-side routing, naturally, are the first-class citizens. Static prerendering is only offered on an opt-in basis, leaving content-driven sites like this one off the happy, default path, even if not far.
Soupault
Soupault has been on my radar for a long time. I’m attracted to its resource-conscious philosophy, the fact that it’s distributed as a single static binary, and its extensible, orthogonal approach to configuration. Moreover, the fact that it treats HTML as a tree instead of an opaque string makes it highly programmable, to a degree rivaled only by Pandoc filters and Pollen in my experience.
I may yet revisit Soupault. Its aesthetic and philosophical appeal are too great for me to ignore. But it didn’t win the day this time around for the admittedly shallow impression that its great programmability is obscured by its TOML interface. I’m sure it’s something I could get used to, but I’m just not that interested in trying today. Wrangling the external binaries to support features like rendering sounded similarly unappealing, even though I agree with the design choice.
11ty
11ty’s appeals to simplicity connected strongly with me. It’s SSG-first, zero-config out of the box, uses file-based routing, and has supreme flexibility in choice of templating language. It has a collections API, which is great for blogs, and, after Astro, is one of the first places I read about island architecture.
I very nearly went all-in on 11ty. What tipped the scale away from it was ultimately that I was just more curious to try out Astro.
Astro
The web framework for content-driven websites
I get the strong impression reading the docs that use cases like mine, with this site, were top of mind in Astro’s design. It is content-driven, server-first, and fast. Its marketing, instead of dwelling on “simplicity” and other such abstract nonsense, emphasizes its batteries-included, DX-driven approach. While this doesn’t speak to me on an emotional level, it’s very pragmatic, and handily covers all my build system criteria in a single package.
Rebuilding
I’d chosen my system, and my plan was ready as it’d ever be to make contact with reality.
The unsurprising first step was
bun create astro@latest -- --template minimal
(Speaking of hopping on bandwagons) that’s right, bun. Again more out of
curiosity than any technical reason. Astro reports partial support, and
it’s been mostly smooth sailing, though I do occasionally find myself having to restart
the dev server (particularly in cases of moving or renaming files). I don’t know whether
this is Astro’s or Bun’s fault.
Routing
Next was deciding on a route layout (when I first got started in the biz, we would’ve been calling this the “information architecture” 🦕).
I’m starting things of very sparsely: / is the list of posts, and /posts/[slug]
shows a post. In the near future I plan to incorporate chronological and tag-based
browsing routes, as well as my old Zettelkasten (which comprises the majority of the
initial 150-document tranche).
Content
With the scaffolding complete, I was ready to to add content. This meant copying over the old posts I deemed worthy of the light of day, fixing up obsolete formatting and shortcodes, and defining an Astro Content Collection:
// ./src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const posts = defineCollection({
// where to find the collection's entries
loader: glob({ pattern: '**/*.md', base: './src/posts' }),
// metadata/ frontmatter schema (drives type checking, LSP)
schema: z.object({
title: z.optional(z.string()), // fall back to <h1>
published: z.coerce.date(),
tags: z.array(z.string()).default([]),
}),
});
// key in `collections` determines collection name,
// recorded in type astro:content.CollectionKey
export const collections = { posts };
Now we just needed a route for each entry in the collection and some markup to render them:
---
// ./src/pages/posts/[id].astro
import { getCollection, render } from 'astro:content';
export const getStaticPaths = async () => {
const posts = (await getCollection('posts')).sort((a, b) =>
// newest -> oldest
b.data.published.valueOf() - a.data.published.valueOf())
return posts.map(post => ({
params: { id: post.id }, // corresponds to [id] URL placeholder
props: { post }, // Astro.props
}));
};
const { post } = Astro.props;
const { Content } = await render(post);
---
<!--
Markup for actually rendering a post. Mine has a few extra bells and whistles, but
this is the essence of it.
-->
<Content />
To add my notes collection, I’ll simply add an entry to collections in
src/content.config.ts and write a similar Astro component to the above.
Style
The last step was to throw on a splash of paint. I wanted it to be readable on desktop and mobile, and support media-query-based dark-mode without me having to do too much. In the past I’ve reached for Pico CSS for this job, but this time around I wanted to try Open Props.
After bun installing open-props, I added:
---
import 'open-props/style';
import 'open-props/normalize';
import 'open-props/buttons';
---
to my main layout component for the batteries-included Open Props experience (that
normalize stylesheet is remarkable!).
Having reached “good enough” territory, the last step was publishing.
Publishing
GitHub Pages seems to have advanced somewhat since I last covered the topic, so I’ll save the details for a future post. In summary, I:
- pushed the source to a new GitHub repository,
- added a GitHub Actions workflow to build and publish the site,
- configured the repo’s Pages settings to use the
willbadart.comdomain, and - updated the
AandAAAADNS records for my domain (with Cloudflare, in my case) to point at GitHub’s IP addresses.
The result of all this is of course the website you’re looking at now!
Reflections
JSX vs. traditional templating languages
JSX is front-and-center in Astro. Its basic unit of composition, the Astro component, is built on it, as is its (plugin-supported) MDX integration. I like the feel of JSX because it treats markup as a data structure, not just a string, and can be programmed with a real programming language (even if that language is JavaScript). It’s also no small thing that it can be type-checked.
My past experiences with, for example, Jinja and Helm chart templating, by contrast, were marred with pain and footguns from ad-hoc languages trying to splice data into strings.
In spite of its support for JSX, 11ty seems to put other templating languages (that seem to fall more into this “traditional” camp) first, which certainly influenced my decision.
TypeScript
I don’t know how big a deal this will end up being on a small site like mine, but I found Astro’s TypeScript support to be very pleasant. I’m impressed, for example, that in:
const posts = await getCollection('posts');
the compiler correctly infers the type of posts from the Zod schema I defined in my
content config. Combined with Neovim 0.11’s improved native LSP support and the
live-reloading dev server, this all makes for a great DX.
Philosophy
At the end of the day, I surprised myself a little with my choice of build system. I’m typically all about simple, composable, orthogonal tools; I don’t think I’d use any of those words for Astro. Maybe I finally got tired of tinkering and wanted to just write again. Maybe this is just a stepping stone to a system that fits my values better. Or maybe this was, ultimately, a valid trade of simplicity for expedience, just one of the innumerable tradeoffs that are so central to the software craft.