./blog.sh

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

Ten smaller things

The rest of this release is ten additions that each need a paragraph rather than a post.

Pages

page: true gives a post a permanent address at the root — /about/ — out of the listings and the feeds, but in the sitemap and the search index. Ghost, WordPress, Squarespace and Substack import them as pages now instead of skipping them, and the import says where they landed and that nothing links to them yet. The engine will not write a nav: entry on your behalf; where a page belongs in your menu is your decision.

Series

series: groups posts and series_part: orders the one you published out of turn. Each series gets a listing, and every post in one links to the previous and next part — within the series only, never spilling into the archive around it. A draft's preview says whether its series name joins something that exists or founds something new, so a typo is caught while it is still free to fix.

Unlisted

Read more

My fixtures agreed with me

The importers had a good test suite. Every source had fixtures, the fixtures were parsed, the results were asserted, and everything passed.

Then I ran the importers over three archives I had not made: a Ghost export of 118 posts and 419 images, a Hugo tree of 77 files, and a WordPress WXR of 194 items. Alongside them, a fourth that is entirely mine — this engine's own export, read straight back in — because that one is not asking whether the code survives a stranger, it is asking whether the two halves of a round trip still agree.

The three foreign ones were the point, though. Fixtures had agreed with the code because both ends of them are mine.

The picture nobody failed to download

{{< figure >}} is how Hugo writes a picture, and the Liquid stripper ate it as a Liquid tag. Both of the ones in that archive pointed at files lying right next to the article in its page bundle: nothing to download, nothing that could fail, and they were gone without a line in the summary — because nobody had tried to fetch them, so nobody had failed.

Read more

./blog.sh 1.3.2

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

A GoToSocial release, and both fixes came from the first site to wire blog.sh to a GoToSocial instance: arch-linux.cz runs its own, instead of pointing at a Mastodon account. My own tests had Mastodon on both ends, so they could never have found either.

GTS speaks Mastodon's API with its own accents, and two places in the engine had taken the Mastodon dialect for the whole language: the feed widget asked every host for JSON first and got exactly that, a JSON Feed the XML parser then reported as malformed; and doctor refused the account id its own setup had pointed you at, because it demanded digits where GTS answers with a 26-character ULID.

One small thing for skins came from the same site: a listing page now says whether it is the first one. <body> carries page-first or page-cont, so a stylesheet can tell the front of a listing from its /page/N/ continuations — something CSS cannot work out on its own, because it cannot read an address.

Read more

Ten versions back

The engine has never had an undo. Editing a post rewrote its file, and what the post said an hour ago was gone — not because anything went wrong, but because a save is a save.

Now the previous text is kept before every overwrite, and [v] in a post's properties puts one back.

Before every overwrite, not just before an edit

A re-import is an overwrite too, and that turns out to be the case where this earns its keep. Editing a post by hand is deliberate; re-importing a source over a post you have since tidied up is the accident that eats an evening's work, and up to now the only recovery was a backup.

Ten per post, newest first.

Picked by recognising it, not by reading a timestamp

The list is one you walk like every other list in the wizard, and the row under the cursor shows what that version said — its title, or its opening words if it hasn't got one. Ten timestamps tell you nothing about which one is the version you want. Ten opening sentences tell you immediately.

Read more

An import only ever adds

Re-importing an archive used to be a thing you did carefully, at night, after a backup. The run went out and fetched every picture again — pictures the archive already held, byte for byte — and each of those downloads landed on top of a file that was already correct.

That is fixed, and the fix is one sentence: a media file's identity is the address it came from.

The archive already knows

Every media entry records its src. A re-import compares what the source offers against what the archive holds, and fetches only what is missing. Over a complete archive that is nothing at all — the second run of the same import downloads not one picture, and says so in the summary.

The source itself is still read, and that is the honest limit of the claim: an export sitting on your disk costs nothing, while a feed or an API is fetched every time, because there is no other way to find out whether anything changed.

Read more