Porting Jekyll to Astro with Claude Code
This blog has been on Jekyll for a decade. Over that time I’ve extended it with plugins and custom scripts, using liquid and Ruby, and hand-rolling it, prior to AI, locking me further into the ecosystem. I don’t install Ruby or Jekyll and instead generate the site using docker containers, however recently those containers started throwing errors specifically in WSL on win11 (no problem on macOS or the linux-based GitHub runners). After failing a number of times to resolve my win11/WSL woes, I cracked it and began the port from Jekyll to Astro.
Why Astro?
To be clear, I don’t know much about Astro. I asked Claude Code for some TypeScript-based recommendations and after some back and forth we settled on Astro. I think the layouts might be somewhat like React but my content remains as markdown files and Astro handles the rest. This is fairly similar to my experience with Jekyll and Liquid and Ruby - reading Liquid is extremely unpleasant and I don’t retain any of the knowledge I spent hacking ruby to get my way, even though I had to do the majority of it before AI code-assist. I am, however, familiar with TypeScript and npm so Astro seemed like a good fit.
The port
With a few prompts - maybe one for the plan - 90 percent of the work was delivered in under 30 minutes. It was astounding how close to the finished outcome the initial result was. That remaining 10 percent though, well that took about four evenings - maybe 2-3 hours each - to get it right. The initial port produced a changeset of 1,000+ files and looking at the rendered output showed a number styling issues and leftover liquid tags. I was particularly impressed with how well the CSS was ported, and it mostly stayed the same - sass, which is not my preference, but it required little modification. Most of the gaps were in the different includes and plugins and getting rid of the liquid tags that were strewn throughout the content.
Eyeballing every rendered output wasn’t going to happen and so I (we) wrote a verification script that compared the HTML output of the Jekyll and Astro builds. This produced a report of differences that I could eyeball for similarities and fix over time. I committed the plan to source and updated CLAUDE.md to know about it on every session. Then, we iterated on running the verification script and choosing areas to improve.
xml
<!-- CLAUDE.md -->
We have performed the [migration](./astro-blog/MIGRATION_PLAN.md) from Jekyll to Astro for this blog.
We are now verifying and fixing anything that was not migrated.
Run `/verify` after any change.
To debug a specific file difference, run from the `astro-blog/` directory:
```bash
npx tsx scripts/verify-migration.ts --diff="/path/to/file.html"
```
**NEVER** modify files in the root, you may only modify files in the `astro-blog/` subdirectory.
The verification workflow I use, /verify, is the same from my post on assist verify, though the scripts used in this case were few - one to run the astro build and verify the migration and another to type check non-astro scripts.
json
// package.json
"scripts": {
"verify:build-astro": "cd astro-blog && npm run build --silent --force && npm run verify-migration -- --silent ",
"verify:typecheck": "cd astro-blog && npx tsc --noEmit",
}
Getting caught up
Where Claude Code can take me most of the way, it still has frustrating moments where for whatever reason it can’t quite get there and I need to change the approach. This can be exacerbated in the case that I too don’t know the solution or am unfamiliar with the framework. One of these moments was attempting to build remark plugins to replace the previous include functionality from Jekyll. Two things burnt us:
astro buildcaches by default, so if the content doesn’t change, the plugin won’t process again and;remarkplugins are configured with a[myPlugin, {}]syntax, whererehypeonly needs[myPlugin].
In an attempt to replace the include I use for figure tags, Opus 4.5 would continually fail to get the plugin to produce anything. Even when it tried to debug, it would see that the plugin was never being called. In its attempts to clear the cache, it deleted so much of the node environment that it corrupted its own claude-code global install. After a couple of hours of failing hard, I took a break and came back by dumbing down the approach severely. I asked it to install a simple plugin it knew, remark-toc, prove that it worked, and inspect its source. This enabled us to solve the configuration and caching issues. After this, I asked it to slowly build the include plugin, one step at a time, verifying each step worked before moving to the next. This latter approach proved that the regex matching used prior was totally broken and contributing to no output.
Often when we get stuck, it’s because I’ve asked for too much at once - though it takes time and experience to know how much is too much with any given model, task, or context. When this happens, break it down - take the smallest, verifiable, steps towards the goal.
The outcome
The port is finished and I’m living in a (preferred) TypeScript world. The site builds faster, but more importantly, looks 99% the same (you’re looking at it right now). Even with the frustrations it was fairly quick and I shudder to estimate how long it would have taken me to port the ruby and liquid code to typescript manually. Key takeaways were:
- let Claude verify its work frequently, perhaps by writing a script to do so and;
- when stuck, break the problem down to the smallest verifiable steps and slowly iterate towards the goal.