./blog.sh

./blog.sh — a minimalist blogging system

./blog.sh is a blog engine you run from a terminal. Posts are files, the site is a build, and the comments live on the Fediverse — there is no database anywhere in that sentence.

Three things make it different from the other five hundred static site generators:

  • No database. One post is one JSON file. Backup is tar, history is git, leaving is a for loop.
  • No dependencies. Ruby stdlib and bash. No gems, no npm, no lockfile. Clone and run.
  • No comment system. Every post is announced on Mastodon or Bluesky; replies to that announcement are the comments, loaded by your browser from the public API.
Read more

A new look: Minimal Terminal

This site has a new look. It is called Minimal Terminal, and it is the page the way a terminal would print it: two typefaces, words in square brackets instead of buttons, a hash before a tag, nothing round and nothing filled except the one thing that is selected.

It is not made only for this site. Minimal Terminal is one of the six skins of the Minimal family that come with the upcoming 1.11, beside Minimal itself, Literal, Journal, Manual and Pictorial. A site switches to any of them with one line, site.skin:, and each takes its colours from the site, so this blue is simply blogsh.app's own.

1.11 is not out yet; this site runs ahead of it to see how the look holds up on real pages. If something looks wrong, a line breaks where it should not or a page reads badly on a phone, reply to this post on Mastodon.

Nothing here for PHP2Shell

On Wednesday PHP shipped emergency releases for two flaws in its core, CVE-2026-109618 and CVE-2026-109619, known together as PHP2Shell. A single request from somebody who never logged in can be enough to run code on the web server, and every PHP installation that serves web requests is affected, whatever runs on top of it. The fixed releases are 8.5.12, 8.4.27, 8.3.36 and 8.2.35; anything older needs a backport from a vendor or a patch built from source.

A site built with ./blog.sh has nothing for it to hit. There is no PHP in it, no database behind it and no admin page in front of it. What the web server hands out are files the build wrote earlier: pages, a stylesheet, a search index, pictures. A request can ask for one of them and get it, and that is all a request can do; nothing on the server runs because somebody asked. That is not a hardening step somebody remembered to take. It is what Built for one blog on purpose meant by "nothing needs a security patch on a Tuesday". This time it was a Wednesday.

Read more

Everything else in 1.10

Six posts have covered what 1.10 is about: the archive that talks back, an icon instead of a terminal, the door for programs, what a listing says now, saved unchanged and a second language, one release later. This one is the rest, in the order an operator meets it. Most of it came from two sites that have run the pre-release since the end of September, and from readers who read the fixes of the fixes.

Read more

A second language, one release later

1.9 brought the second language: one post, a text in each language, an /en/ tree beside the root. The first site to run it for real was the one the engine grew up on, and a month of that showed what no test shows. The 1.10 changelog has over thirty entries about the second language. This is not their list. It is about what a second language needs that the first one does not.

A name that inflects

site.author stands in sentences: "na osobním webu Daniela Šnora". Czech inflects a name, so the configuration holds the genitive, and the pages in English took it over, case and all. 1.10 adds site.author_name: the name as it stands alone — under a hero title, as the person the structured data names, on the pages of a language other than the site's own. Without it author is used everywhere, as before. A language that does not inflect never needed it, which is why nobody wrote it.

A series has a name in every language

Read more

Saved unchanged

A typo fixed in one paragraph. That is the whole change the author made, and that is how the archive should record it: one paragraph different, the rest as they were. On a real archive, by the road a phone sends posts along, the engine could not do that up to 1.9. Measured on October 8: 101 posts changed by being saved unchanged.

What markdown cannot say

A post is stored as blocks, and the editor shows text. Between the two stands a conversion that mostly loses nothing — a paragraph is a paragraph, a heading is a heading. But an archive that came from an import carries things on its blocks that markdown has no word for: a paragraph Tumblr marked as indented, as a list item or as a line of a chat; a video's provider and the address it was first seen at; a player a service handed over as HTML. The text the editor shows carries none of it. And a save used to rebuild every block from that text: fixing one typo rewrote every other block of the post without them. At the desk and from a program alike; only at the desk nobody thought to measure it.

The question the other way round

Read more

What a listing says now

A listing is the page the engine builds most often and nobody writes. The front page, a tag's page, a year of the archive, the parts of a series, the search results — all of them are cards of posts one after another, and what a card says is the engine's decision, not the author's. 1.10 changes several things on listings at once, and one of them changes without anybody setting anything. That one is worth being ready for.

The card of a picture post carries the picture

A card is cut by height and keeps its first block whatever it weighs. So a photograph under one line of words fell off the card of exactly the post that is that photograph: on one archive the first page of the pictures showed three pictures in thirteen cards. For a post whose kind is image or video the medium now goes first on the card, and the same cut runs over what follows. A picture taller than wide is held to 22rem instead of taking a screen, and the card is charged that height, so the caption under a portrait photograph is on the card too.

Read more

The door for programs

Up to 1.9 the engine had one door for programs, and it was narrow. The phone delivered a post through it: a shortcut sent a file over SSH, receive.sh took it on the other side and answered with one object. Two commands kept that promise — add --json and publish --yes --json. Everything else was a screen for a person: the listing, a post's properties, the queue, the trash. A program that wanted to see what was in the archive had nothing to call.

1.10 opens the door wide, and that is exactly why it is careful about what may come through.

Every screen as data

Every screen a person reads before acting, and every action it offers, now has a --json form: one object on standard output and nothing else, every key always present. A refusal is an object too, {"ok": false, "error": "not_found", …}, with a zero exit — the status says whether an answer arrived at all, the object says what it was. A program that has to test whether a key exists is a program writing its own parser; that is what this prevents.

Read more

An icon instead of a terminal

Every guide to this engine starts with the same sentence: open Terminal, go to your site's folder and type ./blog.sh. For somebody who lives in a terminal, that is one sentence. For somebody who does not, it is three things to get wrong before the first word is written — where Terminal is, what "go to the folder" means, and why the command starts with a dot and a slash.

1.10 turns that sentence into something that can be double-clicked.

What the wizard offers

./setup.sh on a Mac ends with one more question:

Put an icon in Applications that opens this blog with a double-click?
Read more

The archive talks back

A letter arrived at the end of September. Pavel had used this engine to back up his old blog, and wrote down what he learned doing it: over a thousand posts in nine years, and stretches when he wrote every day. He had not known. The blog had been his the whole time and the posts had been in it the whole time, and still the number came from the archive, the first time anything read it end to end.

This release is about what an archive says when it is asked. How things get into one, how they leave it and how it is checked, this site has written about enough. This is about what the archive knows about you.

What the archive counts

./blog.sh stats reads every post and answers with one screen. On the archive this engine was built around, today it reads:

Read more

./blog.sh 1.10

./blog.sh 1.10 is out — release on GitHub.

The release that opens a door for programs. Every screen you read before you act, and every action it offers you, can now answer as data too. A program on another machine reaches them over the SSH the server already has, and only through one script that lets it run one engine command and nothing else. It connects with a code ./blog.sh pair draws in the terminal, so nobody types an address or a key anywhere. This is what the native apps for iOS and Android, now in preparation, are built on.

Around it, the archive says more about itself. An "On this day" card shows posts from this day in earlier years, /series/ is an index of every series, a page of a listing says which days it covers, and the 404 page offers a search already filled in with the words of the address it could not find. On a Mac, ./setup.sh offers an icon in Applications, so the blog opens without typing into a terminal. And the second language from 1.9 has grown up: a series has a name in each language, and a translation keeps its old addresses the way a post does.

One thing ahead of time: the /write/ page and the SendPost shortcut go away in 1.11, and the apps take their place. In 1.10 they work as before.

Read more

Where this engine grew up

Every feature on this site has a page that says what it does. Fewer say why it is there, and almost none say what the week before it existed was like. That part was written somewhere else: on sean.cz, the archive this engine was taken out of and the site every change lands on first — 6,655 posts going back to 2003. Until last week it was written in Czech only.

Since 7 October sean.cz reads in English as well: 126 posts, each the same post as its Czech original with the words in a second language, the way a second language works here. Among them is the whole series that follows this engine from the day the site moved onto it, The new Sean.cz: twenty-one parts so far, July to October.

A few places to start, if you want a sample rather than the whole run:

Read more

Building an Archive

Steven D. Brewer has been blogging since 2002: PHPWiki, then Drupal, then WordPress. With Drupal 7 at the end of its life he wanted a home for twenty-four years of posts that would need no maintenance, and put them on blog.sh: archive.bierfaristo.com, with the Drupal posts and the Twitter feed already in, and pieces of an old Esperanto-USA blog recovered from the Wayback Machine.

Worth a read if what you're after is an archive that has to outlive the software it was written in.

#blogsh #archive #drupal

The page a stranger opens first

Saturday, LinuxDays in Prague, a stand run by friends from OSCloud — Mastodon, Pixelfed, PeerTube, Matrix, the services for people who want their data where they can see it. A full day behind one table, and not a single talk seen. This engine was there for the first time in person: on a leaflet, and in conversation whenever the Fediverse came round to "and where would I write?"

Four squares on an A4

The leaflet is one sheet. Four panels — write, move, deploy, show — each with a terminal excerpt and a QR code. None of the codes lands on a page made for the event. Each one opens a tag page this site already had: /tag/authoring, /tag/migration, /tag/deploy, /tag/appearance. The site's own archive, sorted by what a stand gets asked, was the landing page. Nothing was built for the day that would be stale the day after.

Read more

How big it can get

It is a fair thing to ask before you hand twenty years of writing to a tool: at what point does it stop being pleasant to use? Measured here in August, on an archive of 6,639 posts, the honest answer was a shrug: adding one post cost the whole archive — forty-eight seconds from pressing enter to the site being live — because every page that so much as mentioned a post had to be remade, whether it had changed or not.

That's no longer the honest answer. Since 1.6, the build keeps a record of what went into each page, and remakes only the ones whose record no longer matches. The archive underneath is the same one this was measured on in August, seven posts heavier: 6,646 now, and the engine three releases on, at 1.9.

What you wait for today

Publishing a post dated today, on that same archive: sixteen seconds of build, two of upload — where it was forty-eight. The build isn't touching pages that didn't need it any more, so the size of the archive stopped being what the wait was made of.

Where the size still shows up

Read more