Codelab
Model

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

FieldMeaning
routePatternThis 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.
parentIdThe layout this node nests under, or null at the root. Only a layout can be a parent.
isHomeThe app's home. Every app has exactly one.
statusAvailability: "draft" (default — not on the published site) or "published".
visibilityGating after publish: "public" or "private". Ignored while draft. Private pages run accessPolicy; a failed gate looks like a 404.
fieldsAuthor-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

  • Applist_pages / create_page run on the active app from set_context.
  • Layout pages — shared chrome (header, nav, footer) plus codelab.outlet. Leaves nest with parentId.
  • Elements — the page is an element tree. container: { _tag: "Page", id: pageId } on add_element / move_element / remove_element.
  • Components — drop in as elements with kind { _tag: "Component", value: componentId }. Bind instance values with update_props on 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

ToolWhat it does
list_pagesLightweight 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_pagePartial: name, status, visibility, type, parentId, routePattern, accessPolicy, SEO, loader. Omitted fields stay. Not isHome — promoting home has to demote the previous one.
delete_pageCascades the page's elements. Cannot delete the home page or the last remaining page.
load_page_outlineIndented tree (kind, id, aria-label, styles/props when set). Heavy — prefer search when you know the target.
find_element_in_pageSearch 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 omitted routePattern. 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" without codelab.outlet is 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 with update_page { status: "published" } or the live site 404s. (packages/modules/page/core/src/domain/page.factory.ts)
  • Same add-element rules as everywhere. aria-label for purpose, omit targetPosition to append, inspect_element_type before Radio / Field / Dialog / Menu. See Element.
  • Do not add_root_element on a page. The body root already exists. (packages/modules/element/adapter/src/ai/add-root-element.tool.ts)

On this page