locks, ledgers, and reading on the ferry
Three smaller things shipped together today, the quiet kind that make the site sturdier rather than shinier.
A lock you can choose. Accounts can now turn on two-factor login: a code from an authenticator app on top of the password. I want to be clear about the philosophy, because it matters to me: nothing here will ever nag you about it. No banners, no "secure your account today!", no forced enrollment. If you never open the security tab you will never know the feature exists. I turned it on for my own admin account, because that account can do everything, and that felt right. For you it is entirely your call.

The enrollment step: a secret for your authenticator app, then one code to prove it works.
I wrote the code-generation part myself instead of pulling in a library, because underneath the scary name TOTP is a small, beautiful trick from an old RFC: take an HMAC, then let the last four bits of the digest choose where to read the code from. The whole heart of it:
mac := hmac.New(sha1.New, secret)
mac.Write(counterBytes[:])
sum := mac.Sum(nil)
// Dynamic truncation (RFC 4226 section 5.3).
offset := sum[len(sum)-1] & 0x0f
code := (uint32(sum[offset]&0x7f) << 24) |
(uint32(sum[offset+1]) << 16) |
(uint32(sum[offset+2]) << 8) |
uint32(sum[offset+3])There are also recovery codes, and a couple of escape hatches so a lost phone never means a lost blog. I tested the ugly paths hardest: the same code twice is rejected, a burned recovery code stays burned, and an account that never enrolled logs in exactly as before, to the byte.
A ledger for the languages. With five languages and near fifty posts, "which translation am I missing" was becoming a memory exercise. The admin languages page now shows the whole grid for posts too, missing and stale cells marked, one click from any gap to the right editor.
And offline reading. The site is installable now, and posts you have opened stay readable with no connection, which around here mostly means: on the ferry. The tricky part was an invisible one, cached pages resolved their assets relative to the wrong URL and quietly broke, the kind of bug that only shows up when you actually turn the network off and look. So that is what I did, and now it works.