Skip to content

./blog.sh 1.6

Choose a tag to compare

@DanielSnor DanielSnor released this 03 Sep 06:22
· 25 commits to main since this release

./blog.sh 1.6 — the release about the time between deciding to publish and the site saying so.

Three things happened here. The build stopped rebuilding what nobody changed. A post can now be handed over as a file by something that is not a person — a phone shortcut, a cron job, a script — and there is a page on the blog itself to write one on a phone. And a photograph stopped being stored twice. Around those: a row of share controls under a post, an icon a tag can carry, a way out of the trash and the versions, and eleven places where the engine behaved differently depending on whether a terminal happened to be watching.

A page already on disk is not built again. The build keeps a record of what went into each page — the post, the template, the locale, the configuration, the engine itself — and renders only the pages whose record no longer matches. Measured on this project's own archive of 6,639 posts, on the server it lives on: a rebuild that changes nothing costs a seventh of what it did, and publishing a post dated today about two fifths. The ratio is what travels; the seconds behind it are one machine's. Backdating into the early 2000s still costs four fifths of a full build, because a post landing there moves every listing page between the front page and where it lands, and those pages genuinely changed. The record is an optimisation and never an authority: anything it cannot vouch for is built the old way, and ./blog.sh rebuild --full renders everything from scratch when you want it to.

./blog.sh add <file> writes a post without asking anything. The same work the wizard does, with the markdown handed over instead of typed. --json answers as one object — slug, path, state, url, warnings — or a refusal with its reason as a code, and leaves with zero either way, because iOS Shortcuts throws away the output of a command that failed and the reason is the whole point. publish: yes in the front matter publishes on arrival, announcement and all — the one thing a file may ask for that the wizard never could. --untrusted refuses a picture reference that is not a bare filename, for markdown that arrived over a wire.

A post can be sent from a phone. scripts/receive.sh takes a whole post over one SSH connection — pictures first, the markdown last — and puts it in incoming/, where add makes the post. Every name is checked and every body decoded before anything is written, so a refusal leaves incoming/ as it found it; a delivery over the ceiling hears too_large instead of a closed channel, and one that goes quiet is dropped. Nothing new listens on the network: it travels over the SSH the server already has, and the key wants a forced command.

And a page to write it on: /write/. Set write: true and the build publishes a small editor — a title, the text, tags, photographs and video, each picture with its description — that wears the blog it writes to: its name, its palette, its language, and its tags offered as you type, the ones used in the last twelve months first. A preview in the blog's own stylesheets, a row of marks above the text, a Draft | Publish switch above the send button. It is served from the blog itself, so the same git pull moves both ends of the protocol; it is off by default, marked noindex, and holds no secret — what sending needs is the key in the shortcut, on the phone. Two iOS shortcuts do the sending, and the server's answer comes back to the same page.

publish <slug> --yes and --no-announce. --yes answers the draft dialog in advance; --no-announce puts the page up and sends nothing to Mastodon or Bluesky, and toot can still send it by hand afterwards. A post dated outside the recent window is published but not announced, and the run says so.

A row of share controls under a post. Off unless share: names what you want, drawn in the order you name it. bluesky, email, facebook, linkedin, threads and x are plain links. mastodon is a button that asks which instance is yours and remembers the answer; copy puts the address on the clipboard; system hands the post to the phone's own sheet. Those three appear only where they can work.

A tag can carry an icon. tag_icons gives a tag one of the eight the engine ships or an SVG of your own, shown in the heading of /tag/<name>/ and on the date badge of every post that has it. The order of the list is the priority, because most posts carry more than one tag. Asked for in issue #45.

A way out of the trash and the versions. empty trash and empty versions say what they are about to remove, in items and in megabytes, and require that count typed back. doctor notes a trash with posts in it, and check names a type: the engine does not know, with the route somebody reaching for a ninth type usually wants — asked for in issue #42.

A published picture is the archive's own file, under a second name. public.nosync/ holds a hardlink now instead of a copy, so the pictures take half of what they did — 1.8 GB on this project's installation. A backup saves nothing by it; that was measured rather than assumed.

Fixed, among twenty others: two posts written at the same instant left one post — a delivery arriving while an import ran, or two phones one after the other; the name is taken by creating the file now, and both survive. A byte-order mark in front of the frontmatter turned the header into body text. Attaching a photo through a symlinked directory in incoming/ deleted the original. An embed could still smuggle a script past the sanitiser in three spellings. A tag with a letter outside ASCII crashed under docker exec and cron, where LANG is unset. And ./setup.sh | tee setup.log echoed the access token in clear text — anyone who set a site up that way should assume the token is in that log and rotate it.

Upgrading — nothing to migrate: git pull, rebuild, deploy. The first build after the upgrade is a full one and writes .build_cache.json in the installation directory, gitignored and always safe to delete. assets/js/share.js is a new file every page loads, so deploy it. The reviews this release went through — engine, page and receiver, each read by someone whose job was to break it — closed every finding with a test that fails on the old code.

Full notes in CHANGELOG.md.

MIT licensed. Documentation · blogsh.app