SIGNAL STRONG · — —:— — UTC
OPERATOR · Stefan Machammer
/ index dispatches devlog-3-keeping-the-bots-out
[ FIELD NOTES / ESSAY ] — transmitted 2026·05·26

Devlog #3 — Keeping the Bots Out

Welcome back! This entry is about one feature, and it's one I built out of necessity rather than ambition: spam protection for comments, in the form of Google reCAPTCHA v2.

Welcome back!

This entry is about one feature, and it's one I built out of necessity rather than ambition: spam protection for comments, in the form of Google reCAPTCHA v2.

The Reason: A Flood of Bots

This wasn't a theoretical "nice to have." I'd been getting a steady flood of bot comments on the site, and the moderation queue was filling up with junk faster than I could clear it. Open comment forms are a spam magnet, and mine had clearly been found.

So I added Google reCAPTCHA v2 — the classic "I'm not a robot" checkbox — to the public comment form. I wanted a real gate without bolting on something heavy.

How I Built It

A few principles shaped the whole thing:

Opt-in. reCAPTCHA needs two keys — a public site key (safe to put in the page, Google's widget needs it to render) and a server-only secret key (used to verify submissions). If either one is missing, reCAPTCHA is simply treated as off. That means a fresh install still posts comments without anyone having to configure anything first — the historical behavior is preserved until you opt in.

Fail-closed. When the secret is set and a comment comes in, the server verifies the token with Google before accepting it. If the token is missing, malformed, rejected, or Google's endpoint is having a bad day and the request errors out — the comment is rejected. A flaky upstream should never silently open a spam window.

The secret stays secret. In the admin settings, the site key is echoed back into the form (it's public anyway), but the secret key is write-only: it's never re-emitted, a blank field means "keep the current value," and clearing it is a separate, explicit "Disable & clear keys" action behind a confirm dialog. There's also a live Enabled/Disabled status indicator so you can see at a glance whether the gate is actually up.

On the verification side, I deliberately don't surface Google's internal error codes to the visitor — every failure shows the same generic "please try again" message, so a bot scripting the form can't learn anything from the responses.

The Small Details That Matter

A couple of ordering decisions that aren't obvious but make a real difference:

  • The reCAPTCHA check runs after the normal form validation passes. No point burning a verify round-trip to Google on a submission that was going to fail validation anyway.

  • When verification fails, the form re-renders with everything the visitor typed still filled in, plus the error — because nothing's more infuriating than a captcha wiping your comment.

The theme side is dead simple: themes get a recaptchaSiteKey value in their render context that's only non-null when reCAPTCHA is fully configured. The default theme renders the widget and loads Google's script only when that key is present. A theme that ignores the field just gets un-gated comments — no breakage, no surprises.

Did It Work?

This is exactly the kind of "plumbing" feature that's invisible when it works — lots of careful failure handling, opt-in defaults, very little that's flashy. But after wading through a spam-clogged moderation queue, getting the boring parts right here felt very worth it. The junk dropped off, and I get my comments section back.

Next up I want to dig into the theme system itself — how templates get linted and where I think it's still too rigid. More on that soon.

Thanks for reading — see you in the next entry!

If this found you,

"there is a channel. One long letter on the last Sunday of the month, sent quietly. No tracking, no autoplay."

Open the channel
Filed by
Admin · operator
Transmitted
2026-05-26 · 17:23 UTC
Slug
devlog-3-keeping-the-bots-out
Carriage
UTF-8 · plain html
Channel
Stefan Machammer

[ COMMENTS · 0 ON FILE ]

No comments on file yet — be the first to transmit.

Open a return channel

Human check

◂ back to the dispatch log