Journal

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.

← More from the blog