This blog is a quarter of a century old now (oh my! 👴), and for the better part of a decade it has run on Jekyll: posts are just Markdown files in a repo, built to HTML and hosted for free - the comfort of a Blogger or WordPress.com, but with the same VSCode and git I use for code, no ads, and nothing to lock me in.
The one part that never fit that model was comments. For years they meant a third-party system (Disqus, Facebook Comments) with the usual ads, lock-in and spying - until Staticman let me keep them in the repo too, as files, approved by merging on GitHub. I liked it enough to write a detailed post on setting it up.
Then the spam came, and Staticman couldn’t stop it: its spam filter wouldn’t connect, and with no updates in sight to fix that, its clever approve-by-merging turned into an unmaintainable chore. So I turned comments off and rethought the problem.
What actually needed replacing
At first I thought I’d have to throw everything away again to replace Staticman - until I realized it has three separate pieces: storage (comment YAML files in the blog’s git repository), rendering (mostly built into Jekyll, since it supports yanking content from YAML), and intake (the server that receives the form, filters spam, and writes the file). Only the intake was broken.
Writing, hosting and maintaining that intake server didn’t sound practical. But it doesn’t need to be a server: it’s just a simple, stateless piece of logic behind an HTTP endpoint - a perfect match for a serverless function, hosted nearly for free in a fire-and-forget fashion.
I chose Cloudflare Workers and had Claude help me build that worker, which has just one job: to take an incoming comment and decide what to do with it.
One job, in layers
That “one job” is really a small pipeline. Cheap checks at the door kill most automated spam before it ever costs me a commit: a honeypot, a minimum fill time (a classic anti-bot heuristic - anything sent back in a second or two is almost certainly a bot), and Cloudflare Turnstile (an invisible CAPTCHA). What survives goes to Akismet and then to Claude Haiku for a second opinion.
The verdict decides the comment’s fate: clean ones publish themselves, disagreements open a pull request for me to review, and confirmed spam is dropped (but logged, so I can catch false positives). The approve-by-merging flow I loved in Staticman is still here, but now it only gets me involved when needed.
Security comes from that same simplicity: the worker uses a GitHub token of mine, scoped narrowly to this job and easy to disable or rotate if it’s ever compromised. Nothing else.
A couple GitHub Actions workflows keep it running without me. One redeploys the worker whenever (and only when) I touch its code, so a dependency bump or a quick fix ships itself. The other two patch a blind spot: because the worker acts as me, GitHub never tells me about the comments it commits or the PRs it opens. So a separate workflow, running as a different actor, assigns me the review PRs and leaves a note on the auto-published ones - and I hear about every new comment.
Make it your own
I’d normally go into the details, or wrap it into a library - but it would likely hit the same maintainability walls Staticman did. So instead I just point at the script and its docs: skim those links (or hand this post to your AI assistant) and you can get something like it for your own site.
I’m very grateful to Eduardo Bouças for building Staticman - I could not have gotten to this point without his work and ideas. But I’m quite happy with what I have now, and hope everyone can also be!
