Series seed

First real use

Ten commits, three plans, and the first post I tried to write in it was rejected.

6 min read

There are ten commits on the 20th of September. The day before had two.

Three separate plans ran through it. The rest of the ghost-editor one, then a plan called editor-polish, then the first phase of one called ghost-parity, which tells you roughly what I was chasing by the evening.

[GAP: what did this day actually look like for you? Sat watching each phase land, or setting one going and doing something else? I genuinely don’t know and I don’t want to invent it.]

What arrived

The morning was the editor becoming an editor. A Tiptap schema that holds every real post on the site, the post list, the writing canvas with debounced autosave, the selection toolbar, the + and slash card menu.

Two details from that commit matter more than the list does. The first is that saving still keeps untouched blocks byte-identical, which is the rule from the round trip carried up into a live editor rather than quietly dropped once there was a UI to admire. The second is one line in the commit body: the editor ignores Astro’s content-driven full reloads, so autosave no longer reloads the page mid-edit.

Remember that one. It comes back in the next part, and it comes back badly.

Then the afternoon, which is the part I’d actually show someone.

The cards are the real components

Every registered component renders in the canvas as itself. Not a grey box labelled “Callout”. The actual Callout, the one that ships, mounted inside the document you’re typing in.

A <Quote> in the editor is the Quote.astro the site builds. Change a prop in the settings panel and the thing in front of you changes, because it’s the same component. Wrapper cards keep their text editable inside, so a <WorkSection> is still a container you can write paragraphs into rather than a sealed block you have to edit somewhere else.

The bit I’m quietly pleased about is how the props get read. A card in MDX carries JSX attributes, { caption: 'Left caption' } and so on, and something has to turn that text into values the panel can edit. The obvious move is eval, or pulling in a parser. It uses neither. There’s a small literal parser in the editor that handles the shapes my components actually use and refuses anything else. No eval, no new dependency.

I’d been using MDX components for years without ever really seeing them while writing. That afternoon they arrived in the page.

The same run added images: drop one in and it gets resized, converted to webp, and written into src/assets/<collection>/<slug>/, so it keeps Astro’s optimisation pipeline instead of quietly bypassing it. And a save now carries the file’s mtime, so if the post changed on disk while I had it open the save is refused with a 409 rather than flattening whatever I did in the other window.

Then I tried to use it

Somewhere in there I made the first real post with the thing. It was, inevitably, a post about building the thing.

invalid slug: Building_The_CMS_for_blog

That’s the whole message. It’s correct, and it’s useless. It tells me the shape is wrong without telling me what the right shape is, and I wrote both the rule and the error, so I had nobody to be annoyed at.

The fix is small and I think it’s the most honest commit of the day. The messages now name the shape and show an example, the New post form carries the same example as a placeholder and a hint, and a missing series folder says that, instead of blaming the slug for something that isn’t the slug’s fault.

There’s a footnote on that commit I like even more. The store test counted forty posts by hand. So the act of writing a new post, in the CMS, made the test suite fail. The test now counts the files on disk like a reasonable person.

That file is still in my content folder, by the way. It says title: "Test". [? Do you want this series naming it, or would you rather I left the scratch file out of it?]

Two more things that only break on contact

Two other fixes that day came from the same place, which is using it rather than looking at it.

A tall card, a chart or a big image, pushed the settings panel far below the card or straight off the bottom of the screen. Obvious the first time it happens, invisible before that. The panel now takes the gutter to the right of the card, or the gutter to the left, or a centred overlay with a scrim, top-aligned so it never covers the thing you’re editing.

That one has an odd detail attached. Codex was rate limited at the time, so a Sonnet subagent implemented it instead, and I reviewed it and checked it in the browser at 1600, 1280 and 375. Which is the first time the pipeline itself had to be improvised around, and it turned out the shape of the work mattered more than which model was doing it.

The other one is my favourite bug of the whole project so far. A wrapper card with no children converted to content: [], and ProseMirror’s block+ rejects that, so any post containing an empty <WorkImageGrid> refused to open with “Invalid JSON content”.

The editor had written that file. Minutes earlier. Then it couldn’t open it.

It was caught by schema.test.ts, which runs over every real post in src/content, on a draft I’d made in the editor a few minutes before. The same pattern as the round trip test from day one: point the suite at the actual content, and the content tells you what’s wrong. It now opens with one empty paragraph and writes back out empty, so the file on disk is unchanged either way.

Where it landed

By the end of the 20th it was a real editor. Cards that render as themselves, markdown while typing, a right-click convert menu, paste a URL and get a link titled from the page itself, paste a YouTube link on an empty line and get a VideoBreakout.

And a small pile of bugs that all had the same cause, which is that I had finally started using it.

None of those were found by reasoning about the code. They were found by typing into it, once, badly.

Which is the argument for the next part. The day ended with a Playwright spec that drives the real dev server, creates a post, types, inserts a card, waits for Saved and reads the file back off disk. It found two things on its first run, and one of them meant a brand new post could not be edited at all.

seed maturity Seed — this note will change as the strip does