Tagcontent

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

Walking the archive

A post here recently argued that the wizard menu got shorter because operations on a post belong in the post, not in a menu. That still holds. This release adds a screen anyway, and the reason is worth stating: finding a post is a different job from doing something to one.

./blog.sh browse shows the same posts list does, as a screen you stay in.

What it is

Arrows and Page Up/Down through the whole archive. Filters by type, state and tag, with the count next to each. And a search that filters as you type.

The search is the site's own. Words are ANDed, "a quoted phrase" counts as one, -word excludes, and diacritics never decide a match. The query language and the folding live in lib/search_query.rb, next to a note that it and assets/js/search.js change together — because a query that means one thing in your browser and another in your terminal is worse than having no search in the terminal at all.

It searches the full text of every post, so under the selected row there is a line of that post's own text showing you why it matched.

Read more

Your camera writes down where you were

Take a photo on a phone and it records the spot you stood on. Not as a guess — as coordinates, accurate to a few metres, in a block of metadata that travels with the file wherever it goes.

Social networks strip that on upload. They have done it for years, quietly enough that most people have stopped thinking about it. A static site has nobody to do it: blog.sh copied your photo into the archive byte for byte, and nothing in the engine had ever looked at metadata at all. So a picture of a cat on a windowsill published the windowsill.

From 1.2.1 it doesn't. A photo loses its location on the way into the archive — when you add it to a post, and equally when it arrives through any of the twenty-two importers, because they all end up in the same write.

Only the location

The camera, the lens, the exposure, the moment the shutter opened: those stay. They are your own record of your own photograph, and an archive that silently threw them away would be answering a question nobody asked.

Read more

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

Read more

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

Read more

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.

Read more

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.

Read more

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.

Read more

No database. One JSON file per post.

Here are three lines of Markdown, and here is what the file on disk looks like after you save them.

What you type:

Posts are **files**. The build is a loop over a folder.

There is no step three.

What lands on disk, one JSON file per post:

Read more