Schema-driven UI: this post is a data structure
This article is not HTML in a file. It is an array of typed blocks that a renderer turns into a page. Here is why that tradeoff is worth it, and where it starts to hurt.
The paragraph you are reading right now is an object. It has a type field set to 'p' and a text field holding this sentence. The whole article is an array of those objects, and a renderer walks the array and decides what to draw for each one. There is no HTML file for this post anywhere in the repository.
What the schema looks like
The vocabulary is deliberately tiny. A block is one of a handful of kinds: a paragraph, a heading, a pull quote, a bullet list, an embedded video. Each kind uses only the fields it needs, and a union type in TypeScript describes the set. The renderer switches on the type field and nothing else.
That is the entire design. Its value is not expressiveness, it is the opposite: the small number of things a post is allowed to be.
Why not just write HTML or Markdown
Markdown was the obvious alternative and I have used it for other projects happily. The reason I did not here comes down to what else the content has to feed.
Every post also has to produce a share card image, an entry in a sitemap, an RSS item, a prerendered HTML file with the right structured data, a category colour, an estimated read time, and a listing card with search and filtering over it. All of those need the post's metadata as data. With a typed structure, the build script reads the same source the site renders from, and it is impossible for the sitemap to disagree with the page.
With loose files you end up parsing frontmatter and hoping every author remembered every field. With a typed array, a missing field is a compile error before it is ever a bug.
The point of constraining the content model is not the rendering. It is that everything downstream of the content can finally trust it.
The properties you get for free
- A new block type is one case in one switch statement, and every existing post keeps working.
- Styling is global by construction, because no post carries its own markup.
- There is no sanitising problem, since the content is not a string of HTML being injected anywhere.
- Read time, tag indexes and category grouping are derived from the data rather than maintained by hand.
- Renaming or restructuring a field is a type error across every post at once, not a search-and-replace you hope you finished.
That last one is the quiet benefit. Content in files rots silently. Content in a typed structure fails loudly the moment the shape changes, which is exactly when you want to hear about it.
Where it hurts
It is worse for writing prose. There is real friction in wrapping each paragraph in an object, and I feel it every time. If the only requirement were publishing text, Markdown would win outright and I would not defend this choice for a moment.
It also punishes anything the vocabulary does not cover. A one-off layout, a table, an interactive widget: each one is either a new block type or a compromise. That constraint is genuinely useful over a year, because it stops the content becoming a junk drawer of bespoke markup, but on the day you want the table it is annoying.
And it does not scale past a single author. Nobody non-technical is going to edit a TypeScript array, which is the honest reason real content platforms put a form in front of a schema instead of exposing the schema itself.
The general shape
This pattern turns up far beyond blogs. Anywhere the interface is generated from a definition rather than hand-built, the tradeoff is the same: you give up bespoke control per item, and in exchange every consumer downstream can rely on the structure.
The failure mode is always the same too. Someone needs one thing the vocabulary cannot express, an escape hatch gets added for raw content, and within a year half the items use the escape hatch and none of the guarantees hold. The discipline is not in designing the schema, it is in refusing to widen it casually.
A schema with an escape hatch is a suggestion. The guarantees only hold while the vocabulary does.
For a one-author site with a build pipeline that needs to trust its own content, I would make the same call again. For anything with more than a couple of writers, I would put a proper editor in front of it and keep the schema underneath, which is exactly what every content platform eventually converges on.