Série./blog.sh v1.1

./blog.sh 1.1

./blog.sh 1.1 is out.

Pin a post to the top. Schedule into publishing slots that queue instead of colliding. Attach files as download cards. Rename a slug without breaking a link — the old URL redirects forever. One dialogue for everything you can do to a post.

And deploy guards that measure the build against the build, so a failed upload can't quietly switch them off.

The post that stays

A blog's front page is the only page that's a statement. Everything else is chronology doing its job.

So pinned: true in a published post's header now holds a copy of it at the top of the first listing page. That's the whole feature, and the interesting part is everywhere it deliberately doesn't apply.

Only the front page

Type listings, tag listings, the RSS feed and the sitemap stay strictly chronological. A pin is a claim about what a visitor should read first, not a rewrite of when things were written. Someone subscribed to the feed gets posts in the order they happened, and a pinned post doesn't jump their queue every time it's toggled.

What happens as it ages

While the post is still recent enough to sit on page 1 by date, it appears exactly once — pinning it doesn't print it twice on the same screen. Once it has aged onto page 2, it appears in both places: at the top of page 1, and in its own chronological spot on page 2.

Číst dál

Some posts are downloads

A photo in a post is a bare filename in an image line. It always has been:

![A view from the window](window.jpg)

So a file attached to a post is the same line without the exclamation mark:

[Reading notes, 2025](reading-notes.pdf)

A whole line that is nothing but a link, pointing at a bare filename with a known extension, is an attachment. It gets staged through incoming/ like a photo, stored next to the post like a photo, and it never leaves your site.

A line with a URL in it stays what it has always been: a link. The difference is the bare filename — the same rule photos already follow, so there's nothing new to learn.

Rendered as a card, not a link

An attachment renders as a card carrying the label, the extension and the size. The size is there because a download deserves to say what it costs before you commit to it — a 40 MB PDF on a phone on mobile data is a different proposition from a 200 kB one, and the reader should get to make that call.

Číst dál

Three drafts, three mornings

Writing doesn't arrive evenly. You get an evening where three posts want out, then nothing for a week. Publishing them the way they were written means three posts in one night and silence afterwards — which is a worse blog than the same three posts spread across three mornings.

So a site can now say when it usually publishes:

publishing:
  slots:
    - "mon 09:30"
    - "wed 09:30"
    - "fri 09:30"

Or, if you write more than that, a single "daily 09:00".

With that in the config, scheduling a draft offers you the next slot no other scheduled post already occupies. Three drafts written in one evening are offered Monday, Wednesday and Friday, in that order, without you counting days.

It only ever offers

This is the part I care about most, because a queue that takes decisions away from you is a queue you fight.

Číst dál

The photo your iPhone won't show anyone

An iPhone shoots HEIC by default. Safari renders it. Chrome doesn't. Firefox doesn't. So a photo straight off the phone, attached to a post and published, is a photo most of your readers will never see.

The old behaviour was the worst possible one: the file went in, the build couldn't measure it, and the image quietly vanished from the page — taking its caption with it. No error. Just a post with a hole where a photo was.

Refused, with the command you need

Attaching a HEIC now stops the save and prints the exact conversion command for the machine you're standing at:

sips -s format jpeg photo.heic --out photo.jpg      # macOS
heif-convert photo.heic photo.jpg                    # Linux

Your file stays exactly where it was, in incoming/. Nothing is deleted, nothing is guessed at. You convert it and carry on.

Or converted, if you ask

Číst dál

An old link still knows the way

The first post on this site is about a photo that disappeared from a 2012 blog entry while the link to it kept working. Link rot is the reason this engine exists, so an engine that quietly broke its own URLs would be a bad joke.

Renaming a post changes its address. That's unavoidable — the slug is the URL. What's avoidable is the old address turning into a 404.

What renaming does now

Rename a post — r in the properties dialog — and the post records its old address inside itself. The build then keeps a one-page redirect standing at every address the post has ever had.

The link in a two-year-old toot keeps resolving. The link somebody put in their own blog post keeps resolving. You get to fix a slug you regret without the fix costing you every reference to it that already exists in the world.

The redirects belong to the post

Číst dál

The menu got shorter

The wizard used to list ten things you could do. It lists five now, and the engine can do more than it could before.

That's not a paradox, it's an admission: eight of those ten were not activities. They were operations on a post, wearing the costume of a menu item.

Everything about a post, in one place

./blog.sh props <slug> — or, in the wizard, pick a post and press v — shows what there is to know about it. State, type, tags, whether it's pinned, whether it's been announced and where. If it's scheduled, the whole publishing queue.

And the actions live there too, next to the facts they act on. A draft offers publish and schedule. A published post offers unpublish, re-announce, pin or unpin, rename, delete.

You no longer go to a menu, choose "unpublish", and then hunt for the post. You go to the post and see that unpublishing is one of the things it can do right now.

What stayed where it was

Číst dál

The guard that switched itself off

This site's deploy has two guards. If a build suddenly has far fewer files than what's live, or far fewer bytes, the deploy stops. They exist because a broken build looks exactly like a deliberate one to rsync --delete, and a static site is only ever one confident sync away from being erased.

In 1.0 they could turn themselves off. Permanently. Silently. Here is how, because the shape of this mistake is more useful than the fix.

The reference was the wrong thing

The guards compared the new build against the deploy manifest — a record of what's on the target. Reasonable, until an upload fails. A failed upload leaves the manifest out of true, and a guard measuring against a record it knows is wrong would fire on every subsequent run.

So there was a marker: after a failed run, stand the guards down until a clean run comes along and restores the reference.

Číst dál