My fixtures agreed with me
The importers had a good test suite. Every source had fixtures, the fixtures were parsed, the results were asserted, and everything passed.
Then I ran the importers over three archives I had not made: a Ghost export of 118 posts and 419 images, a Hugo tree of 77 files, and a WordPress WXR of 194 items. Alongside them, a fourth that is entirely mine — this engine's own export, read straight back in — because that one is not asking whether the code survives a stranger, it is asking whether the two halves of a round trip still agree.
The three foreign ones were the point, though. Fixtures had agreed with the code because both ends of them are mine.
The picture nobody failed to download
{{< figure >}} is how Hugo writes a picture, and the Liquid stripper ate it as a Liquid tag. Both of the ones in that archive pointed at files lying right next to the article in its page bundle: nothing to download, nothing that could fail, and they were gone without a line in the summary — because nobody had tried to fetch them, so nobody had failed.
That is the shape of every finding in this pass. Not crashes. Silence.
The promise that held for three per cent
Images come from the tree itself — no network. True of a Jekyll site, and repeated as a general promise about markdown trees in four places: the docs, the README's comparison table, the importer's header, and the comment above the adapter.
Of 79 image references in that real Hugo archive, 72 were absolute URLs pointing back at the WordPress the site had been migrated from years earlier. The import made 72 requests, 66 answered 404, and 20 of the 74 posts arrived with no pictures at all.
The summary reported all of that honestly. The documentation had told the reader there was nothing to report, so there was no reason to read it. All four places now say which half needs the network, give the number from that archive, and tell you to import while the old host still answers.
A fifth of a site with URLs in its headings
[](url) is what a WordPress-to-Markdown conversion leaves behind where a heading anchor used to be. The parser required at least one character of label, so it matched nothing and printed the raw markdown to the reader: 153 heading blocks across 14 of those 74 posts, square brackets and a full URL in the heading, five of them welded onto the end of the actual words. The table of contents linked to anchors named after github.com.
And a few more
A video uploaded to Ghost was lost and three lines of its player were published in its place. A percent-escaped filename was slugified character by character, minting permanent addresses beginning ef-bf-bc. An article that showed markdown inside a code block had its example eaten — and chasing that turned up something worse underneath: a paragraph ending in a picture came back a newline short, so the fence under it stuck to the text and every fence from there down was read inside-out, code as prose and prose as code. On that archive, 30 of its 72 images each did that to everything below them.
What the HTML converter has to drop — players, iframes, forms — was counted by exactly one of the eleven adapters that use it and thrown away by the other ten, so a WordPress import named its losses and a Jekyll import of the same body said "Done."
The lesson is cheap and I keep re-learning it
A fixture I wrote encodes what I believed when I wrote it. A stranger's archive encodes what actually happened to their blog over fifteen years, on platforms that changed under them, exported by a tool that had its own bad day.
Only one of those two can tell me I am wrong.

Comments