Releases: DanielSnor/blog.sh
Release list
./blog.sh 1.0.1
./blog.sh 1.0.1 — a bug-fix release.
No new features, no config changes, nothing to migrate: git pull and you are on it. What is here came out of a systematic audit of every flow the engine has — authoring, publishing, the two cron jobs, the build, deploy and the importers — asking one question of each step: if this fails here, can the flow still finish, and is anything lost? It turned out the answer was sometimes no, and twice the loss was permanent.
Nothing can lose a post any more
- Editing a post into a different year destroyed it. The old JSON was deleted before its media was moved, and moving into a year that had no media directory yet raised
ENOENTin between — leaving the post in neither year, not intrash/, and no longer in the editor's temp file. The replacement is written before anything is removed now. - A failed write truncated the post it was rewriting.
File.writeempties the file first and only then discovers it cannot write, so a full disk turned the post being saved into 0 bytes. Every post write (and the deploy manifest) now writes a sibling file and renames it into place, so a failure leaves the previous version untouched. - A new photo could overwrite an existing one. Media files are numbered per post, but a kept image did not consume its number, so adding a second photo of the same type handed the new file the kept one's name — the copy overwrote it and both blocks then showed the new image.
- A new post could land on a leftover media directory and inherit its photos while its own upload was deleted from
incoming/. - A missing source file cost the copy that was already live. The build dropped it from its output and the
--prunethat every publish runs then deleted it from the site — for a source that may have been missing only for a moment.
Flows that could not finish
- The scheduled-publish cron could wedge permanently. Publishing a post with photos into a year nothing had been published into yet (a New Year's post, a backdated draft) raised
ENOENT, took the whole batch with it — including posts already published and announced in that run — and repeated on every tick from then on. - One imported post could stop the site from building at all. Inline formatting offsets were counted against the raw HTML text but stored against a whitespace-collapsed copy, so ordinary pretty-printed source markup pushed a span past the end of its text and the build died with an error that named no post.
- The sidebar refresh never uploaded anything on any site that does not configure all five widgets: the file list was built with
ls, whose non-zero exit underset -euo pipefailkilled the script one line before the upload. Silently, every half hour. - An interrupted deploy locked out every later one. The partial manifest made the file-count guard read the next ordinary deploy as an explosion in size and refuse it — including the deploys the CLI runs for you, which cannot pass
--force. - A closed stdin span the CPU forever. The wait-for-photos loop treated end-of-input as "not yet" and re-checked at full speed until someone killed the process.
- One unreadable post file took down everything at once — the build,
list, every picker and the cron — with a parser error that named no file, so the commands you would reach for to find it were the ones that had stopped working.
Quieter things that were simply wrong
- An edit saved across the cron tick that published the post reverted it to a scheduled draft and dropped its announcement URL, so the next tick announced it a second time. Such a save is now refused, with the text kept.
- An ambiguous slug was resolved again at every internal step, so one command asked "which year?" two or three times — and an inconsistent answer retargeted it, up to and including deleting the other post.
- GIF and WebP images were silently dropped from every page, caption and all, because the engine could not read their dimensions and treated that as a 1×1 tracking pixel. Both formats are read now, and an image whose size cannot be determined renders instead of vanishing.
- An import preview promised more posts and media than the real run wrote, and a WordPress export with an unusual
post_namecould write an invisible post. - A feed that answers slowly forever could hold the build and the sidebar cron indefinitely; there is a 30-second deadline on the whole request now.
- An emoji-only tag rendered as a link to
/tag//, which goes nowhere. --forcedeploy forgot the files pending deletion on the target, which could then never be pruned.
Small additions
./blog.sh version(also--version), the version in the wizard banner and in the User-Agent the engine sends — it used to say1.0in a string literal and would have kept saying it forever.- The backup checklist in operations now names
assets/images/header.pngandfavicon.png: they are gitignored on purpose, so nothing else keeps a copy and a restore would have quietly brought the site back with the shipped default artwork.
Upgrading
git pull, then rebuild and deploy. Nothing to migrate. On an existing site the only visible change is that images the engine previously could not measure now appear — on the 3280-post archive this was developed against, that was exactly one photo, and the old and new engines' output differed in 14 files in total, every one of them accounted for.
MIT licensed. README · install · operations · importing · localization
./blog.sh 1.0
./blog.sh 1.0 — the first stable release.
A minimalist, file-based blog engine: posts are JSON files, the site is a static build, authoring happens in a terminal, and comments live on the Fediverse. No database, no admin server, no PHP. Ruby stdlib and bash — zero gems, zero npm (one documented asterisk: the optional Pixelfed/RSS widgets need rexml, a default gem some distros package separately).
What 1.0 ships:
- Content model — one post = one JSON file of typed blocks (text, headings, quotes with attribution, lists, task lists, tables, code, chat, images, video, audio, links, rules). Inline formatting stored as offsets into plain text, so nothing ever parses Markdown twice.
- Authoring — an interactive CLI wizard:
add,edit,publish,schedule,unpublish,delete/restore,toot,rebuild,preview,list. Drafts deploy to hidden, shareable preview URLs with a QR code in the terminal.incoming/staging for writing from a phone over SFTP. Degrades to plain line prompts in a pipe, respectsNO_COLORandTERM=dumb. - Markdown — a deliberate subset with a cheat sheet generated by the parser itself, localized, including a section on what's deliberately not supported and why.
- Build — static HTML via ERB, no framework. Tag and content-type archives, anchored pagination, RSS, sitemap, client-side search (phrases,
-exclusion, diacritics-insensitive), memoized rendering that writes only changed files. - Comments & announcements — every published post announces on Mastodon or Bluesky (never both); replies are the comments, loaded by the browser from the public API.
unpublishdeletes the announcement too. - Deploy — six backends (Surfer, local, rsync, git-pages, rclone, SFTP) behind one manifest-driven diff with SHA-256 checksums,
--dry-run, opt-in--prune, and shrink/growth guards that stop a broken build before it wipes a live site. - Import — eight sources (Bluesky, Instagram, Tumblr, Mastodon, Pixelfed, Twitter/X, WordPress, RSS/Atom), verified against real archives. Media downloads next to its post; the origin becomes a tag; re-imports update in place and never duplicate.
- i18n — English, Czech and German. A language is one YAML file with per-key fallback; adding yours is the easiest way to contribute.
- Appearance — a complete theme from 7 colour keys per mode (light + dark), config-compiled CSS, banner with corner-scoped scrim, automatic photo grids with lightbox.
- Security by subtraction — CSP meta, self-hosted assets, zero third-party requests from the engine,
noindexdrafts, escaping everywhere.
Seen in the wild: blogsh.app — the project site, which runs on this engine and demonstrates every feature above in its posts — and sean.cz, the original deployment it was built around.
Docs: README · install · operations · importing · localization
MIT licensed.