Text

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 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

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 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

No gems (one asterisk)

./blog.sh needs no gems. Here's the one asterisk on that sentence, because a claim without its exception isn't worth much.

The claim first: zero gems, zero npm packages, no Bundler, no lockfile, no node_modules folder quietly gaining weight in the dark. Ruby 2.7 or newer and bash. Clone it and it runs.

The asterisk, up front

Two optional sidebar widgets — Pixelfed and generic RSS — parse XML with rexml. That's a default gem: it ships bundled with every normal Ruby installation, so for most people the claim holds as written. But some Linux distributions split Ruby into packages and put the default gems in a separate one.

So: if you enable those two widgets on a distro-packaged Ruby and the build complains about rexml, you're the asterisk. Two ways out — install your distro's ruby-rexml-ish package, or gem install rexml. Either way it's one package, once, and only if you use those widgets at all.

That's the entire dependency story. I'd rather write it out than round it down to zero and wait for someone's build to disagree.

Číst dál

What ./blog.sh doesn't do

A feature list tells you what a tool wants to be. This is the other list.

Features that aren't there — and aren't coming

  • Multiple authors — the engine is built around one person's workflow, and every multi-author feature taxes the single author with roles, permissions and attribution UI
  • Plugins — a plugin API is a promise to keep internals stable forever; a tool this small should be forked, not extended
  • A WYSIWYG editor — your $EDITOR took you years to configure; I'm not going to compete with it in a textarea
  • Its own comment system — storage, moderation, spam, GDPR, and a database, all to rebuild what the Fediverse already does in public
  • Themes as downloadable packages — seven colour keys and your own banner get you further than a theme marketplace, at zero marketplace

Unchecked checkboxes, as rendered by the engine's own task lists. They'll stay that way.

What the parser refuses

Číst dál

Comments I don't host

The star, boost and reply counters sitting right under this post's title belong to a toot, not to this site. And the comments at the bottom of this page are that toot's replies. That's the whole system — here's how it works.

When a post is published, the engine announces it on Mastodon or Bluesky — one network per site, never both; the build refuses a config with two, because comments live in exactly one place. Replies to that announcement are the comments: your browser loads the thread straight from the network's public API, no login, no key, no middleman. The counters under the title are the same numbers, read from the same toot.

What falls out of that

No comment database — nothing to migrate, back up, or moderate at 2am. No spam filter, because the network already has one, and it's better funded than mine. The comment count is just the reply count. And moderation is your instance's moderation: block, mute and report all work exactly where the conversation actually happens.

Číst dál

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:

Číst dál

Built for one blog on purpose

This is not a general-purpose engine, and pretending otherwise would waste your afternoon.

./blog.sh was built around exactly one deployment: one author, one archive, a terminal, and comments on the Fediverse. That's usually the part a project hides on the "about" page. I'd rather lead with it, because it's also the explanation for everything opinionated about this tool. A general-purpose generator solves the general case and then makes you configure your way back to your specific one. This engine started at the specific case and never left.

So here's the honest table:

Číst dál

./blog.sh — minimalistický blogovací systém

./blog.sh je blogovací systém, který se spouští z terminálu. Příspěvky jsou soubory, web je výsledek sestavení a komentáře žijí na Fediversu — v celé té větě nikde není databáze.

Tři věci ho odlišují od ostatních pěti set generátorů statických stránek:

  • Žádná databáze. Jeden příspěvek je jeden soubor JSON. Záloha je tar, historie je git, odchod je smyčka for.
  • Žádné závislosti. Standardní knihovna Ruby a bash. Žádné gemy, žádné npm, žádný lockfile. Stačí naklonovat a spustit.
  • Žádný systém komentářů. Každý příspěvek se ohlásí na Mastodonu nebo Bluesky; odpovědi na to oznámení jsou komentáře, které tvůj prohlížeč načítá z veřejného API.

Takhle to vypadá:

Číst dál