Tagphilosophy24

The archive talks back

A letter arrived at the end of September. Pavel had used this engine to back up his old blog, and wrote down what he learned doing it: over a thousand posts in nine years, and stretches when he wrote every day. He had not known. The blog had been his the whole time and the posts had been in it the whole time, and still the number came from the archive, the first time anything read it end to end.

This release is about what an archive says when it is asked. How things get into one, how they leave it and how it is checked, this site has written about enough. This is about what the archive knows about you.

What the archive counts

./blog.sh stats reads every post and answers with one screen. On the archive this engine was built around, today it reads:

Read more

Where this engine grew up

Every feature on this site has a page that says what it does. Fewer say why it is there, and almost none say what the week before it existed was like. That part was written somewhere else: on sean.cz, the archive this engine was taken out of and the site every change lands on first — 6,655 posts going back to 2003. Until last week it was written in Czech only.

Since 7 October sean.cz reads in English as well: 126 posts, each the same post as its Czech original with the words in a second language, the way a second language works here. Among them is the whole series that follows this engine from the day the site moved onto it, The new Sean.cz: twenty-one parts so far, July to October.

A few places to start, if you want a sample rather than the whole run:

Read more

The page a stranger opens first

Saturday, LinuxDays in Prague, a stand run by friends from OSCloud — Mastodon, Pixelfed, PeerTube, Matrix, the services for people who want their data where they can see it. A full day behind one table, and not a single talk seen. This engine was there for the first time in person: on a leaflet, and in conversation whenever the Fediverse came round to "and where would I write?"

Four squares on an A4

The leaflet is one sheet. Four panels — write, move, deploy, show — each with a terminal excerpt and a QR code. None of the codes lands on a page made for the event. Each one opens a tag page this site already had: /tag/authoring, /tag/migration, /tag/deploy, /tag/appearance. The site's own archive, sorted by what a stand gets asked, was the landing page. Nothing was built for the day that would be stale the day after.

Read more

./blog.sh: blogovací engine, kterému stačí terminál

./blog.sh on Root.cz, the Czech Linux site, in Czech and in my own words: why the text and the pictures belong on your own disk, what a build is and where its result can live, how comments ride on the Fediverse, and why twenty-two importers read an export rather than an API. It also says plainly who the engine is not for: more than one author, anyone who wants a web admin, plugins.

Read more

The parts that don't rebuild themselves

A laptop dies, or a server gets rebuilt from nothing, and the question that actually matters is narrower than it sounds. Not "how do I back this up" — "which of these files does the engine actually need me to protect, because nothing else here can make them again on its own."

Most of an installation doesn't qualify. The engine itself is a git clone; pull it again and it's the same engine. Everything the build writes comes back with one rebuild, given what's left.

The instinct is to back up the site itself — the folder a browser actually sees. That's precisely the one thing this list leaves out. public.nosync, the build's own output, is exactly as disposable as the engine that made it: one rebuild reproduces it byte for byte, so keeping a copy of it would just be storing the same information twice, under two different names, and trusting the older copy by accident.

What's left, once the engine and the site it builds are both set aside, is a short list.

What's actually irreplaceable

Read more

The lock that doesn't wait

Twice an hour, on a large enough site, two cron jobs reach for the same folder at almost the same moment: the scheduled publish, building and deploying whatever post came due, and the sidebar refresh, rewriting a few of the same output files with fresh numbers. Most ticks miss each other by minutes. On an archive big enough that a build and a full deploy outlast the interval between them, some don't.

A deploy walking a tree that another run is still rewriting can serve a page half-written. A run pruning orphans can delete, as an orphan, a page the other run published a second earlier. Neither failure announces itself — the site just ends up quietly wrong.

The lock, and what it refuses to do

One file, .blog-sh.lock in the project root, stands between those two runs — and it makes a choice that looks wrong until you see the alternative. A run that finds the lock already held does not wait for it.

Read more

What's left to get wrong

There's no /admin on this site. No login form to try a password against, no session cookie to steal, no plugin list to check against last month's CVEs — not because a checklist said to strip them out, but because none of them were ever built in the first place.

The engine's own architecture notes put the rest of it plainly: the threat model is a static site with no server of its own. Nothing here listens on a port. The goal was never to harden a complex system against everything that could go wrong with it. The goal was to avoid needing one in the first place.

What's left to get wrong, once that's true, is narrower than a normal blog's list — what the pages carry, what the working directory holds, and the one thing that does arrive from outside.

The one thing that arrives from outside

Writing from a phone means one door does accept bytes from a stranger's own device: scripts/receive.sh, running as the forced command of an SSH key. It listens on nothing — no socket, no daemon, just a key that can run exactly this script and nothing else.

Read more

Whoever loads last wins

Two real sites wear a look this engine never shipped. sh.cynicky.blog is dressed as Ghost — a full-width lead card, a dotted frame, tiles instead of a plain list. arch-linux.cz is dressed as a Hugo theme, Blowfish specifically — a compact bar, a profile block, cards laid out like someone else's homepage entirely. Neither forked the engine. Neither edited a template. Both still take a plain git pull.

One stylesheet, loaded last

site.extra_css in config/site.yml names a path, or a list of them, loaded after the engine's own colors.css and site.css. That's the entire mechanism — a skin is one file of yours that repaints what's already on the page, and the engine never writes to it, so an update can never conflict with it.

Local paths only: every page carries style-src 'self', and a stylesheet on another host would just be dropped by the browser with no error anyone would ever see — the build refuses a remote path out loud instead, before that silence has a chance to happen.

Read more

The build doesn't know where it's going

Somebody builds a whole site on a laptop, imports a decade of old posts into it, previews every page — and none of it has gone anywhere yet. Nothing in env.sh says where, and nothing else is waiting on that: the build runs, the archive checks out, every page renders at localhost. The build doesn't know where the site is supposed to end up, and it doesn't need to.

The same build can start on a laptop, move to a rented VPS a year later, and land on a Cloudron instance after that. Nothing in the content pipeline changes between any of those moves. What changes is the answer to one question — where should the files go — and six things can answer it.

What decides it

DEPLOY_BACKEND in env.sh names the target; the values under it depend on which one. Everything past that line is identical across all six. Each backend keeps its own manifest — the SHA-256, size and modification time of every file it has already uploaded — so a deploy only ever sends what changed since the last one. deploy-web.sh --dry-run reads that manifest without touching anything.

Here's what that prints, run just now against this site's own backend:

Read more

Written on a train

A reader who was thinking about moving to this engine wrote to say he would stay where he was. He manages his blog from a phone, on the road, and a blog that can only be worked from a terminal was no use to him however fast it built. He was right.

1.6 answers him with two things, and with one thing it refuses to do.

A page on the blog itself

Set write: true and the build publishes a small editor at /write/: a title, the text, tags, photographs and video, each picture with its description. It wears the blog it writes to — its name, its palette, its language — and it offers the blog's own tags as you type, the ones used in the last twelve months first, so a tag is tapped rather than spelt a second way. A preview shows the post in the blog's own stylesheets. A row of marks above the text wraps what is selected and comes off again on a second tap. Above the send button, a Draft | Publish switch.

Read more

An archive you cannot see is not empty

This site's approved comments and its post counters are two small JSON files, refreshed by a cron job every half hour and uploaded with the rest of the site.

Through 1.4, a tick that ran while the archive was not there would narrow both files to nothing and upload them. An absent or empty content directory answers with an empty list and no error, and an empty list reads exactly like "every announced post was deleted". The live site lost every approved comment and every counter at once. Exit 0, nothing said, nothing mailed.

The archive lives on a synced volume. A volume that has not mounted yet, or a working copy whose files are still arriving, is not an unusual Tuesday.

Not being able to tell is a reason to keep everything

That sentence is the fix, and it now runs in two tiers.

If the content directory is not there — or if the archive reads as empty while the previous files held anything at all — nothing is narrowed, and the run says why.

Read more

Posts nobody meant to publish

An importer's worst failure is not losing a post. It is publishing one.

1.5 went through all eleven import adapters and the machinery underneath them looking for exactly that, and found it three times over, in three different files, made the same way each time.

An archive is the whole account

A Mastodon export is not your public timeline. The outbox holds your followers-only posts and your direct messages beside the public ones — and every standalone toot in it was being written as published. On the archive this was measured against that is 141 posts of 2,548, of which 132 are direct messages, each given its own page, its own sitemap entry and its own item in the feed. Nothing in the run's summary mentioned it.

The scope was in the export all along, exactly where ActivityPub puts it. Public in to is public. Public in cc is unlisted — a real address, out of the listings. Followers-only and direct become drafts: that keeps the author's words in the archive and off the site, which is the only reading of "not public" that cannot publish something by accident.

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