All Articles

Things may look a little different

I rebuilt this blog on boxlang-express, my own Express.js-style BoxLang framework, moving from a CommandBox droplet to a thin DigitalOcean App container, with static assets on Cloudflare R2 and a homegrown MySQL access log replacing GoatCounter. Also: DismalThreads, my BoxLang/CBWire Reddit-style forum, is getting decommissioned end of month, no real users beyond my own test accounts, though the code may go public on GitHub.

Website Redesign

I had the chance to redesign my blog back in March — rebuilding it as Chronicler, a ColdBox 8 and CBWire 5 CMS built specifically for my BoxLang stack — and once I shipped it, I never really found the time to come back and touch it again.

Lately I've been doing a lot of work on boxlang-express, an Express.js-style web framework for BoxLang, and this blog was the obvious guinea pig to actually use it on. Chronicler was a ColdBox application running under CommandBox on a DigitalOcean droplet — a stack I'd been running for about two years at that point. My static assets were being served off that same droplet too, and cleaning that up had been sitting on my list for a while.

Porting the blog over to boxlang-express felt like the right moment to rethink the whole deployment, not just the code. Instead of a long-running monolithic server, the site now deploys as a containerized DigitalOcean App, built from a Dockerfile based on commandbox:boxlang and kept as thin as I could get it. boxlang-express runs its own HTTP server directly, rather than sitting behind CommandBox's usual Runwar servlet container — which also meant one less thing to hand-tune: run straight from the CLI, and the JVM just uses whatever RAM the container actually has, no heap flags required. I moved my static assets off the droplet too, over to Cloudflare R2, so the app itself no longer has to double as a file server.

One thing that came up almost immediately after the switch: boxlang-express doesn't ship with any real request logging — just a single line printed to the console when a request comes in (method, path, IP), nothing persisted anywhere. Chronicler never had to care, since I'd been leaning on GoatCounter for analytics — a script tag on every public page, plus a dashboard card that pulled aggregated stats back from GoatCounter's API. It worked, until it didn't: those API calls were synchronous and rate-limited, and a slow or throttled response from GoatCounter meant a slow or hung request on my end. Not a tradeoff I wanted to keep making.

So I built my own — an actual Apache-style access log, stored in MySQL. The middleware sits first in boxlang-express's chain, ahead of everything else, so it wraps the whole request/response cycle: every request this server handles gets a row, static assets included, not just the page views a script happened to catch. The part I was most careful about is the part that caused the GoatCounter problem in the first place — nothing in the request path ever touches the database. Each request just drops a row onto an in-memory queue, and a background thread flushes it to MySQL on a timer. A slow or unreachable database delays when the row shows up, never the response the visitor is actually waiting on.

Getting the real HTTP status code required one small addition to boxlang-express itself — a getStatusCode() accessor on the response object that simply hadn't existed until something needed to read it back out. Everything else — method, path, duration, IP, referrer, user agent — was already sitting there for the taking.

With the data actually landing somewhere I could query, GoatCounter's usefulness on this site mostly evaporated. The insights dashboard now reads straight from the log — including things GoatCounter never gave me at all, like real server-side response times — and I've dropped the GoatCounter beacon from the public pages entirely. There's a plain log-browsing page too, tucked away in the admin area, and it's turned out to be a far more useful debugging tool than a visitor-analytics dashboard ever was.

Now that every request actually carries a real duration, I finally have an honest answer to "how fast is this thing?" — something GoatCounter, watching from the browser side, could never have told me. The health check DigitalOcean's platform hits every ten seconds to confirm the container's alive comes back in 15-20ms, which tracks — it touches no datasource and renders no view, by design. A real page, rendering server-side against actual MySQL queries, lands more in the 100-400ms range, with the insights dashboard running slower since a single page load there fires off several aggregate queries at once. That's also where "slowest pages" earns its keep: a small leaderboard of which URLs run hottest on average, something a client-side beacon had no way to see at all. I'm not chasing milliseconds on a personal blog, but it's nice to actually know the numbers instead of guessing.

The whole thing now runs on DigitalOcean's smallest App Platform tier — $10 a month, 1 vCPU, 1GB of RAM — and it's plenty. For a personal blog, it's a noticeably lighter footprint than the droplet it replaced, and deploys are now just a git push away instead of an SSH session.


DismalThreads

DismalThreads was another site that came together for one reason: because I could. It's a BoxLang/CBWire-powered forum, borrowing heavily from Reddit. Since I'm essentially the only real user, it doesn't make much financial sense to keep it running. It's been live for almost a year now, and it's still mostly my own test accounts plus a handful of users I cajoled into signing up. I liked having a place to post interesting things I found around the internet as a way to engage with other people — but when there's no one there, there's no engagement.

With that in mind, I'm decommissioning DismalThreads at the end of the month, which gives me time before then to save the content I don't want to lose. Once things are cleaned up a bit and identifying details are stripped from the code, I'll probably put it up as a public GitHub repo, if there's any interest.