September 21, 2026 · 2 min read
A page is data
This page is not HTML. It is a small tree of named blocks, checked against a catalog and drawn by the site’s own views. How foldkit-composition works.
A page builder that stores HTML has already lost. Every redesign is a find and replace across markup nobody remembers writing, and nothing can tell a good page from a broken one until someone looks.
foldkit-composition stores what the author chose instead. The home page of this
site is a document like this one, trimmed:
{
"format": 1,
"roots": ["hero", "latest"],
"nodes": {
"hero": {
"block": "Hero",
"props": { "title": "A blog that explains itself" },
"regions": { "actions": ["read"] }
},
"read": {
"block": "Button",
"props": { "label": "Read the blog", "href": "/site/blog" },
"regions": {}
},
"latest": {
"block": "Section",
"props": { "heading": "" },
"regions": { "body": ["posts"] }
},
"posts": {
"block": "PostList",
"props": { "heading": "Latest writing", "count": 3 },
"regions": {}
}
}
}Blocks, regions and a catalog
A Block is a kind of thing a page may hold: a Hero, a Section, a Heading, a list of the latest posts. Its props are a Schema, and its regions say what may go inside it:
export const Section = Block.define('Section', {
Props: Schema.Struct({ heading: Schema.String }),
regions: { body: Region.many({ accepts: [Content.Flow] }) },
provides: [Content.Section],
})A Section’s body accepts flow content, such as a Heading or a Text, and a Section is itself section content, which is what the top of a page accepts. The Catalog lists the Blocks a site offers. It is code, deployed with the site. The Document is data, stored in the page’s row. The Catalog says what may exist; the Document says what does.
Checked, not trusted
Because a page is data, it can be checked. Composition.validate(Site, page) lists
every problem with where it is: a block the Catalog does not have, a prop of the
wrong type, a Heading where only sections may go. A page’s publish input carries
that check, so a broken page cannot be published, whoever sends it.
Edits are data too. Inserting, moving, duplicating or changing a prop is an Operation,
and Composition.apply refuses one that would break the page, changing nothing.
That is what the page builder sends when you drag a block.
Drawn by the site’s own views
The site draws a Document with a Renderer: one view per Block. The same Renderer draws the published page, the studio’s preview and the builder’s canvas. Change a view, and every page drawn with it changes, including every page already published.
When a Block is renamed or a prop reshaped, stored pages move with a migration:
Composition.migrate renames blocks and props, or rewrites a node, when a page
is loaded or before it is published.