The way out ships with it
An engine that cannot hand your writing back is a hosting service with extra steps. The importers have always pointed one way — twenty-two sources, everything coming home. ./blog.sh export is the other direction, and it should have been there first.
What comes out
The whole archive as a tree of markdown files with YAML front matter, in the layout Jekyll reads: _posts/2026-05-01-slug.md, _drafts/, pages at the root, media under assets/<year>/<slug>/. Old addresses travel as redirect_from, in the shape the jekyll-redirect-from plugin already understands, merging both kinds — where the post lived on the platform it came from, and where it lived here before a rename. Point a static generator at the folder and it builds.
./blog.sh export ~/my-blog-export # everything, drafts included
./blog.sh export ~/public-copy --no-drafts
./blog.sh export ~/somewhere --dry-run # counts, writes nothing
This site, counted rather than written:
Would write to /tmp/bsexport: 43 post(s), 0 draft(s), 0 page(s), and 24
media file(s) (27.6 MB).
9 block(s) markdown has no syntax for were written as HTML: link (6),
audio (1), file (1), video (1). Any engine that passes HTML through renders them,
and this engine reads them back whole -- each one carries its own
definition in a comment above it, which everybody else ignores.
The layout is Jekyll's: _posts/, _drafts/, pages at the root, media
under assets/. Hugo, Eleventy and Astro read it with a line of config;
./import.sh reads it as it stands.
What markdown has no word for
Plenty. An attachment with a label and a size, a video with a poster, a gallery, an embed — markdown has no syntax for any of it. Those blocks go out as HTML with their definition in a comment above them:
<!-- blogsh:block {"type":"file","label":"..."} -->
Any other tool sees ordinary HTML and renders it. ./import.sh reads the comment and gets the block back whole. That is the difference between an export that lets you leave and one that lets you move: export from one machine, import on another, and the installation arrives with its attachments still attachments.
It reads the archive, not the site
No build, no network, no env.sh. It walks content.nosync/ and writes. Which means it works on the day the config stops parsing, on a server you are being evicted from, on a clone you found in a backup — the moments when needing an export is not hypothetical.
The one guard: a directory that already has something in it is refused until you repeat the command with --force, which then writes alongside what is there. An export never deletes anything, at either end. A directory holding only dotfiles counts as holding something — a freshly cloned repository used to read as empty and get written into without the --force the guard exists to require.
Why it is in the engine and not in a script
Because a tool nobody maintains is a promise nobody keeps. The round trip — export, re-import, compare field by field — is in the test suite, and it earned its place there immediately: the first honest run found that an attachment came home as a paragraph with a link in it. Label gone, size gone, block type gone, file orphaned in the tree. Nothing had reported a loss, because both ends of the fixture were mine.
That is the argument for shipping the exit with the engine. A way out nobody ever walks is not a way out — and walking this one, field by field against what went in, is the only reason that defect was ever found.

Comments