The component data model

A single JSON tree is the source of truth behind every site. Here's its shape and why every editing layer writes through it.

One tree, three schemas

Every Zitegen site is a structured tree, not a pile of .astro files. The shape is defined as Zod schemas in the @zitegen/component-model package and nests three levels deep:

  • SiteModelsiteId plus an ordered array of pages.
  • PageTree — one route: pageId, path (like /about), title, and a single root node.
  • ComponentNode — the recursive building block every element maps to.

A ComponentNode carries: id (a UUID), type (the element kind), props (HTML attributes, including the class string), styles (per-breakpoint CSS), states (optional hover/focus overrides), content (text for leaf nodes), html (raw markup for custom-code nodes), dynamicValue (build-time values like the current year), sharedComponentId (marks a Reusable Component instance), and children.

What a node's type can be

Standard HTML tags (section, h1, a, img, form, ul, table and SVG primitives, and more), plus three special kinds: dialog (a native modal), html (a verbatim custom-code escape hatch), and text (a bare, unwrapped text run).

data-zitegen-id: the bridge

A node's id is what ties the JSON tree to the rendered page. When the compiler emits an element, it writes that id onto the markup as a data-zitegen-id attribute, and per-node styles target it with a high-specificity html [data-zitegen-id="…"] selector.

That attribute is the anchor for every editing layer: it's how the visual editor knows which node you clicked, how the style panel scopes a change, and how the AI targets an element to revise. IDs are minted server-side when a tree is created, so they stay stable as you edit.

Styles are stored per breakpoint

A node's styles aren't one flat object — they're split into mobile, tablet, and desktop maps of CSS property to value. The compiler emits them as a desktop-first cascade: desktop is the base with no media query, tablet adds a max-width: 1023.98px override, and mobile a max-width: 767.98px one — emitted last so it wins on small screens.

This is why a style-panel change is always scoped to the breakpoint you're editing, and never silently leaks across sizes. Pseudo-state overrides (hover, focus) live in states and follow the same three-tier pattern.

Every layer writes through one path

The tree is the single source of truth, and every writer funnels through one server-side function — updatePageTreeCore. AI chat, the canvas and style panel, code-side saves, and the MCP tools all hand it a tree; it checks access, validates against ComponentNodeSchema, and persists — recording a version as it goes.

Because there is exactly one chokepoint, the layers can't drift out of sync or corrupt each other's work. Whatever made the last edit, the Source view, a download, and the next AI prompt all see the same tree.

Validate before you write

If you're producing trees programmatically (via the MCP tools, say), run them through ComponentNodeSchema.safeParse() from @zitegen/component-model first. Invalid output is rejected at the write boundary anyway — catching it early gives you a clearer error.