From Jekyll to a POSIXish Static Site Generator
I started this blog back in 2021 and it has been generated by Jekyll since then. As far as personal tech blogs go, that’s probably a record.
These days I don’t use Ruby to write code I own so having to keep it around and up-to-date just for this was more trouble than I was willing to go through.
I considered replacing it with gojekyll. It implements just enough of Jekyll for things to work and, like the name implies, it’s written in Go. That means it’s faster and you can just download a binary and drop it into your bin/ directory to have it working.
That seemed like the best route but then I remembered a limitation of Jekyll that I didn’t like: the fact you can only have a single collection of posts. So, I can’t have two “blogs” on the same instance. Also, I wanted a more interesting solution.
So, instead, I decided to whip up a couple of LLMs to build myself something nice. Based on my experience, the better use of coding LLMs comes from having a top-of-the-shelf model generate a plan for a lesser model to implement. I passed this prompt to Fable 5.1 (high):
This is my personal website and blog which uses the Jekyll static site generator. I want to switch from Jekyll to a custom system that uses only tools that can be found on a POSIX-compliant system like sh, awk, sed, …
Ideally, the system must be fully self-contained in this repo. This mean we’ll need to implement, at least, a markdown to html renderer and an http server for development (jekyll serve). Only if this is nigh-impossible should we use a third-party library.
Existing URLs MUST be kept stable. E.g. /_posts/2025-04-06-the-insanity-of-being-a-software-engineer.md is accessible at /2025/04/06/the-insanity-of-being-a-software-engineer/ and must be kept like that. Same for the other files.
We don’t need SCSS. Just convert our style files to plain CSS.
We don’t need to support Jekyll plugins but we need to keep the functionality we already have. Namely:
- CSS inlining
- RSS feed
- SEO fields
- Generated
sitemap.xml
Jekyll supports a single posts dir at /_posts/. It would be great if could have any number of these. E.g. I may want to add another “blog” separate from my existing one for TILs.
Liquid is used for templating. We do need a templating system but it doesn’t need to be as powerful as Liquid. It just needs to implement what we use. If needed, we can opt for a different syntax (e.g. for frontmatter).
You might want to add tests. Use the self-contained version of https://codeberg.org/sstephenson/brat#embedding-brat-in-your-project
Write the plan to PLAN.md. Use 2 space indents. Max line-length of 80-chars.
And then I had Opus 5 (medium) implement the plan.
The result was pretty cool. It implemented a bunch of things in AWK:
- A Markdown to HTML renderer that supports all of the syntax I’m using – which is most of it. It includes a lexer for syntax highlighting code blocks.
- A small template engine that parses the subset of Liquid syntax I’m using.
- Site and dir settings in
.conf files instead of YAML. These can just be sourced into shell scripts. - Automatic RSS feed and Sitemap generation.
- An HTML and CSS minifier. I didn’t ask for it but I was using Compress HTML in Jekyll. Fable decided to include it in the plan.
AWK was built to transform text. It takes input text, manipulates it and turns it into (different) output text. So, it makes sense it was used.
To orchestrate all the AWK scripts, it wrote a bunch of POSIX shell scripts.
It solved the multiple collections issue. Now, if I drop a collection.conf into a dir with a couple of fields, that whole directory becomes a collection.
The only hiccup came in bin/serve. Fable’s plan was to use good old netcat to open multiple sockets to be served by a shell script that returned the requested file with the appropriate headers like Content-Type, Content-Length and so on. Unfortunately, this didn’t work.
Chrome opens several parallel connections to your server so it can request many assets in parallel – at least in HTTP 1.1, maybe it’s different for later versions. This is fine. But it turns out it does more than that. It may also speculatively open additional connections and just hang on to them. They may or may not eventually be used. This is great for users because if the connection’s needed it can immediately be used, no need to wait for TCP and SSL handshakes. But it cause the netcat process to block.
I had to compromise and settled on a forking web server written in Perl. Interestingly, while this did make our new system less POSIXy it didn’t make it that much less portable. It turns out that Perl is available by default on a bunch of platforms. macOS has it by default. Ubuntu as well. Even Arch includes it in base. And the web server won’t execute when deploying the website on Netlify so that part’s still all very POSIX.
Speaking of Netlify, this change came with an unexpected bonus. The build time decreased from 3m20s to just 12s. That’s almost 17x faster. I think that’s mostly due to the fact that Ruby does not need to be installed as part of the deployment.
To ensure correctness, the output of jekyll build was used as a reference and several tests were written against it. A total of 90 tests were added to cover most functionality. Brat, authored by Sam Stephenson of Rails fame, is used as a test runner. It’s totally self-contained in a single script.
Overall, I’m very happy with the result. Instead of feeling like a blog, now this static site feels like living software. I chose not to make everything open-source just because I’d prefer not to make my drafts public. You can probably issue the prompt above on your own Jekyll repo and get a similar result.