Page
A routed container whose element tree is what visitors see at a URL.
A Page is a routed tree inside an app. Metadata (name, segment, role, publish state) plus an element tree. The published URL is not stored as a string — it is the chain of routePattern values walked down parentId.
packages/modules/page/core/src/domain/page.model.types.ts
packages/modules/page/core/src/domain/page-tree-router.domain.ts
What it is
| Field | Meaning |
|---|---|
routePattern | This node's segment, not a URL. "about", "events/$id", or "" for an index leaf. Stored with no leading slash ("/about" → "about", "/" → ""). $name segments become {{page.params.*}}. |
type | "page" — a leaf that renders content. "layout" — a named folder that wraps children through exactly one codelab.outlet. Layouts never match a URL alone. |
parentId | The layout this node nests under, or null at the root. Only a layout can be a parent. |
isHome | The app's home. Every app has exactly one. |
status | Availability: "draft" (default — not on the published site) or "published". |
visibility | Gating after publish: "public" or "private". Ignored while draft. Private pages run accessPolicy; a failed gate looks like a 404. |
fields | Author-declared props — the page arm of RuntimeContainer. Platform inputs (id, appId, params, search) are merged at read; they are not in this array. |
packages/modules/page/kernel/src/page-type.enum.ts
packages/modules/page/kernel/src/page-status.enum.ts
packages/modules/page/core/src/domain/page.fields.ts
A new page always gets a body root. Add children under that root with add_element — do not call add_root_element on a page.
packages/modules/page/core/src/domain/page.domain.ts
A layout must contain one codelab.outlet (and only one). Without it, nested pages have nowhere to render. Promoting a page to "layout" does not insert the outlet — add { _tag: "Primitive", value: "codelab.outlet" } yourself, then set children's parentId.
packages/modules/page/core/src/domain/page-tree-constraints.ts
The layout's own URL is an index child: a "page" under that layout with routePattern: "". Empty is not a layout path; a layout must have a segment ("dashboard", "events").
packages/modules/page/adapter/src/ai/create-page.tool.ts
Inside the page tree, {{props.*}} is this page's RuntimeContainer (entering a component replaces that frame). Ambient route facts are also {{page.id}}, {{page.appId}}, {{page.params.*}}, {{page.search.*}}.
packages/modules/expression/core/src/domain/expression.namespaces.ts
packages/modules/container/core/src/domain/runtime-container.model.ts
How it relates
- App —
list_pages/create_pagerun on the active app fromset_context. - Layout pages — shared chrome (header, nav, footer) plus
codelab.outlet. Leaves nest withparentId. - Elements — the page is an element tree.
container: { _tag: "Page", id: pageId }onadd_element/move_element/remove_element. - Components — drop in as elements with kind
{ _tag: "Component", value: componentId }. Bind instance values withupdate_propson that node.
MCP tools
All of these need contextId except the outline/search tools that take a pageId (those check you own the page).
packages/modules/mcp/server/src/page.mcp.ts
| Tool | What it does |
|---|---|
list_pages | Lightweight list: id, name, routePattern, type, parentId, isHome. No elements. Rebuild the tree from parentId. |
create_page | { name, routePattern?, type?, parentId?, isHome? }. Omit routePattern to derive from the name — omit is not "". "" is an index page. |
update_page | Partial: name, status, visibility, type, parentId, routePattern, accessPolicy, SEO, loader. Omitted fields stay. Not isHome — promoting home has to demote the previous one. |
delete_page | Cascades the page's elements. Cannot delete the home page or the last remaining page. |
load_page_outline | Indented tree (kind, id, aria-label, styles/props when set). Heavy — prefer search when you know the target. |
find_element_in_page | Search by aria-label, text, or kind → elementIds. |
Then mutate the tree with the element tools, passing container: { _tag: "Page", id: pageId }.
Layout workflow:
create_page { name: "Dashboard", type: "layout", routePattern: "dashboard" }
add_element kind: { _tag: "Primitive", value: "codelab.outlet" } # plus chrome
create_page { name: "Dashboard home", routePattern: "", parentId: <layoutId> }
update_page each leaf { parentId: <layoutId> }Gotchas
- Segment vs URL.
"events/$id"is this node's piece. The published path is the parent chain (/events/42). Re-parenting rewrites descendant URLs for free. (packages/modules/page/core/src/domain/page-tree-router.domain.ts) ""vs omittedroutePattern. Omit → auto-derive from the name ("About"→"about"). Pass""only for an index leaf. A layout cannot use"". (packages/modules/page/core/src/application/page.command.ts)- Outlet is required, not inferred.
type: "layout"withoutcodelab.outletis rejected on placement. Exactly one outlet. (packages/modules/page/core/src/domain/page-tree-constraints.ts) - Draft is invisible. New pages are
draft+public. Publish withupdate_page { status: "published" }or the live site 404s. (packages/modules/page/core/src/domain/page.factory.ts) - Same add-element rules as everywhere.
aria-labelfor purpose, omittargetPositionto append,inspect_element_typebefore Radio / Field / Dialog / Menu. See Element. - Do not
add_root_elementon a page. Thebodyroot already exists. (packages/modules/element/adapter/src/ai/add-root-element.tool.ts)