Text

A slug is an address, not a name

For most of this project's life, a post without a title had two names. Its own page called it by its date. Everything else — the browser tab, the link card somebody shares, every item in the feed — called it burtiky-opekame-hipstamatic-oggl-jane.

That is not a corner case. On the archive this engine is measured against, 2,754 posts of 4,418 carry no title. Sixty-two per cent of a site, going out into the world under an address.

A slug is made out of the words. It is not the words.

A slug is a machine's answer to "where does this live": lowercased, stripped of diacritics, joined with hyphens, cut to the first eight words. Every property that makes it a good address makes it a bad name. It has no capitals, no punctuation, and no idea where the sentence it was cut from was going.

1.5 takes the name from the same place the slug came from — the post's own opening — and stops in a better place.

Where it stops

Read more

./blog.sh 1.5

1.5 is the release where the site learned to say what it holds. The release is on GitHub.

A post that never had a title used to be called by its address — on this project's own reference archive that was 62 % of it. Now it is called by its own opening words, everywhere except its heading, which stays the date it has been since 1.3.

Two new pages come with it: /archive/, a map of the whole site in two levels, and /tag/, every subject it has ever written about. Both are built from the posts themselves — nothing to configure and nothing to keep up to date.

Two things a post can now decide for itself: where its teaser stops (//--more--//, asked for in issue #35), and how much of it a listing card shows — cards are cut before they are written rather than drawn in full and hidden with CSS. And a code block carries a copy button, asked for by an operator running a blog of terminal how-tos.

Read more

Tested like an enemy

1.4 went through four adversarial audit rounds before the tag: fleets of independent reviewers over real archives — real exports, a real 4400-post site, real deploy targets — each told to break the engine rather than confirm it. The findings numbered over a hundred. Every one was reproduced before it was fixed, and every fix is pinned by a test that demonstrably fails on the code it fixed.

The pattern that kept repeating

The uncomfortable lesson: each round's fixes were where the next round found its bugs. A fix for a comment-parsing rule deleted a hundred lines of somebody's config; a fix for that hid a widget's answers; the recovery advice written to save posts could destroy one. Four times in a row, the newest code was the most dangerous code in the tree — which is exactly why the last round audited nothing but the fixes themselves. It caught three regressions of mine before any user could.

Eighteen files

Read more

What a crash may cost

The queue can now swap two scheduled posts that share a slug across two years — with their media and their edit history riding along, because those are keyed by year and slug exactly like the post file. That sentence is short; making it true was most of a release cycle. The interesting part is not the swap. It is what happens when the power goes out in the middle of one.

The budget is zero

A move is several renames, and a crash can land between any two of them. The rule 1.4 holds to: whatever the moment, a crash may cost you the move — never a post. Files step aside under parking names before anything writes; a failed write puts everything back, byte for byte; a hard kill that strands a parked file leaves it where check finds it and says truthfully what it is.

The advice is part of the system

Read more

The other half of check

./blog.sh check reads the whole archive and says what is wrong: media a post asks for and hasn't got, links to addresses nothing answers at, two posts that would be served at one address. Through 1.3 that was where it ended — a list of findings and an exit code, with the fixing left to you and a text editor.

--repair walks it with you

check --repair is the checker's other half: for each finding it offers the one repair that finding allows — an old address written into the target post's redirect_from, a relative link rewritten to the address it means, an orphaned file moved to the trash. Nothing is applied without a keypress, nothing is deleted outright, and a finding with no obvious answer — two posts colliding, an image only a human can judge — is shown and passed over rather than guessed at.

It proved itself before it shipped: on a real archive it took 238 findings down to 42, and the 21 repairs it proposed matched what a person had already decided by hand, one for one.

--json says everything

Read more

Deploy in your own language

The wizards were translated from the start; the narration around them was not. A Czech site watched its own deploy go by in English, one line above a Czech sentence — build warnings, announcement failures, import errors, the queue's repair instructions, all of it. Not broken, exactly. Just a house where half the rooms answer in a different language.

Sixty sentences moved home

1.4 finishes the job: the deploy's whole narration — header, progress, the closing tally, every degradation notice — plus all eleven build warnings, every Bluesky and Mastodon failure, and the sentences an import prints when a source dies. English, Czech and German carry the same set, and the suite enforces the parity, so a key cannot exist in one language and abort in another.

What stays as it arrived

The diagnostics inside the sentences do not get translated: an HTTP code, a server's raw answer, an exception's text. Those are evidence, and evidence is quoted, not paraphrased — the sentence around them is the site's, the thing inside it is the network's.

Read more

The same language, spoken differently

Comments here are replies to a toot, and GoToSocial speaks Mastodon's API — so comments on a GTS instance should have simply worked. They did not, not once, and nobody saw it fail: arch-linux.cz, the first site to pair this engine with its own GTS instance, had never shown a single comment under any post. My own tests had Mastodon on both ends, so they could never have caught it.

Dialect is not language

Two accents were in the way. GoToSocial writes a status address in its own shape, and the engine's pattern insisted on Mastodon's — a numeric id where GTS answers with a 26-character ULID. And GTS requires a token even to read a public thread, where Mastodon hands it to anyone who asks. The first accent broke the address parsing; the second broke the entire idea of live comments, which is a visitor's browser fetching the thread — a browser you cannot give a token to.

Moderation is the door in

Read more

The card that reads Codeberg

The sidebar has a commits card. Until now it read GitHub, full stop — which is a strange default for an engine whose natural habitat is the Fediverse, where the code lives on Codeberg or on somebody's own Forgejo, and GitHub is the mirror you keep for one widget's sake.

One of the first people outside this project to run the engine asked the obvious question, and the answer shipped: widgets.commits.instance takes the server's address, and that is the whole configuration.

One key, not two

There is no forge_type beside it. An address already answers what kind of host it is — Gitea and Forgejo speak the same API, and a key that must agree with another key is a config mistake waiting for its moment. Leave instance out and the card reads GitHub, exactly as before.

Cheaper than the original

Read more

./blog.sh 1.4

./blog.sh 1.4 is out — release on GitHub.

The widest release since 1.0. Three things arrived: the commits widget reads Gitea and Forgejo — asked for by one of the first people outside this project to run the engine, because most of the Fediverse keeps its code on Codeberg, and mirroring to GitHub for one sidebar card was the workaround. widgets.commits.instance takes the server's address; that is the whole configuration. Comments work on GoToSocial — found on arch-linux.cz, where no post had ever shown a single comment, silently; my own tests had Mastodon on both ends, so they could never have caught it. And the engine now speaks the site's language everywhere: a Czech or German site used to watch its own deploy and build warnings go by in English, one line above a translated sentence. Sixty-odd sentences moved into the locales.

Read more

The link is the player

Embedding a video usually goes like this. Open the platform, click Share, click Embed, copy the blob of HTML it hands you, paste it into your post. What you have just filed in your own archive is somebody else's markup: an iframe, sometimes a script, a couple of tracking parameters and a class name from a design system you don't use. And when the platform changes its embed path — they do — every post you ever wrote that way breaks on the same afternoon.

Here you paste the address you would send a friend.

Two of them, actually playing

A video, with a caption:

Read more