In my last post I mentioned a CMS project almost in passing. It has since become the thing I work on most evenings, so it deserves its own.
The site it drives is yoga.drewing.de, where Steffi Drewing publishes yoga and mindfulness classes in Neumünster. The obvious answer would have been WordPress. I didn’t want WordPress — I didn’t want a PHP stack to keep patched on someone else’s shared hosting, and I didn’t want a database query standing between a visitor and a page that changes maybe twice a week.
So the architecture is a little unusual. The CMS itself runs on a Raspberry Pi 4 on my desk: a Go backend, a React admin interface, PostgreSQL in a container. It isn’t reachable from the internet. When content changes, a generator renders the entire site to static HTML and rsyncs it to the shared hosting that serves the public domain. The public site runs no application server and no database — just files, delivered in a few dozen milliseconds. No admin login facing the open web, no plugins to patch, nothing to go down at three in the morning.
That design has one real cost: bookings. People need to sign up for classes, and a static site can’t take a form submission. The hosting can run CGI, so the generator writes a handful of Perl scripts alongside the HTML. A booking travels from the form to one of those scripts, which sends a mail; a daemon back on the Pi reads that mailbox over IMAP, writes the booking into Postgres, and triggers a republish so the number of free places on the public page is current again. It’s a strange pipeline — a mailbox as a message queue — but it’s the only route that crosses from the public web back to a machine the public web cannot reach.
Two lessons stand out from the past months.
The first is that "validate your input" means something more specific once a script sends mail. A value that ends up in a mail header has to be validated as that kind of value rather than merely escaped, and anything deciding where or what gets sent has to come from the database rather than from the request. Those generated scripts are covered by tests that run the Perl syntax checker over every one of them before deployment, because a typo in a generated file is otherwise found by a visitor.
The second is about silent failures. Recently the terms-and-conditions page went online containing nothing but its navigation. A bug had detached the text from the page; the content was still sitting in the database, every build was green, and nobody noticed for days. Builds that pass while the output is wrong are the expensive kind. Now every render is compared against the version currently published, and a page that loses most of its visible text isn’t written at all — the old version stays up and the build fails loudly. The same reflex produced a test that drives the booking form in a real browser with real clicks before every commit, after a rendering change once removed a heading the booking dialog depended on and quietly killed every signup.
The most recent addition is a calendar of all upcoming class dates on the booking page, colour-coded per course. It contains no JavaScript at all. Each month is an anchor target and the month tabs are ordinary links, so the back button, reloading and sharing a link to a particular month all work without a single line of script. I enjoyed that constraint more than I expected to.
Claude Code wrote a good deal of this, which is the thread running back to the last post. It’s very good at the parts I find tedious and entirely willing to ship something plausible and wrong, which is why so much of the work above is about making wrongness loud.
None of this is clever. It’s a CMS, and the world has enough of those. But it’s mine, it costs nothing to run, and the site it produces is faster than a yoga studio page has any business being.
© 2026 by Ingmar Drewing
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0) license. Unless otherwise noted the author of the content on this page is Ingmar Drewing
Soweit nicht anders explizit ausgewiesen, stehen die Inhalte auf dieser Website unter der Creative Commons Namensnennung-NichtKommerziell-KeineBearbeitung (CC BY-NC-ND 3.0) Lizenz.