A blog whose articles are block documents rather than walls of text — paragraphs alongside tables, charts and figure rows. Markdown in, blocks out, deterministically. One core behind an HTTP API, a Flow Block and an MCP preset, so a person in the editor and a model over MCP write the same kind of post under the same rules.
Run
wippy run butschster/blogbutschster/blog
A blog whose articles are block documents rather than walls of text — paragraphs sitting alongside tables, charts, figure rows and callouts in one ordered stream. Self-contained: its own posts, its own page, its own API, its own MCP surface.
What it is for
Two problems, and they turn out to be the same problem.
A machine that writes needs somewhere to put the writing. A pipeline that ends in "here is your markdown" ends too early: somebody still has to paste it into a CMS, and the moment they do, the machine's output and the published article stop being the same object. An article created over HTTP, over MCP or from a Flow is the same kind of thing as one typed by a person, because all of them go through one core.
An article that could have shown something and only told it is worse. Prose alone gives an author no way to show: three numbers that carry the point stay a sentence, a comparison stays a paragraph. Blocks give them somewhere to go, and let the finished piece be seen as it will appear instead of imagined from markdown.
The model
A post is a title, metadata and an ordered list of blocks, each with a type and its own data. What a block looks like is defined once, beside its normalizer. Saved edits are append-only revisions: any revision can be compared with the current article, previewed and restored without erasing newer history — which is what makes an automated writer safe to point at a live post.
Writing
Two ways in, both ending in the same document: markdown, parsed into blocks server-side and deterministically — what a writing model produces reliably — and blocks, for what markdown cannot spell. Sending both is the normal case.
An unknown block type or a misspelled field is dropped, never raised: a bad payload costs one block, not the page. Every write returns an outline of what was actually stored, so a caller can check rather than assume.
The catalogue asks to be asked
What this blog can render is written down once, beside the normalizers, and
everything else is generated from it: the BlockCatalogue tool, GET /blog/blocks, and the author's briefing. A catalogue copied into a prompt
goes stale the first time a type is added — and it goes stale silently, the
model writing old field names and the article just coming out thinner.
Surfaces
- Page —
/blog: the index on the left, the article as it will look on the right. - HTTP —
GET /blog/blocks;GET/POST/blog/posts;GET/PATCH/DELETE/blog/posts/{id};POST /blog/posts/{id}/publish; revisions with detail and restore. - MCP — preset
blog: ten tools, fromBlockCatalogueandCreatePosttoPublishPost. - Flow — the
Create blog postBlock, idempotent throughexternal_ref.
The same core is behind all of them, so a post created by a Flow is refused for the same reasons, and comes out the same shape, as one created by hand.