Page Nodes & Layout
Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source:
crates/cheng-nodes/src/nodes/builtin/ui,crates/cheng-nodes/src/nodes/builtin/ui/common/types.rs,chengflow-ui/src/runtime,chengflow-ui/scripts/generate-pages.ts
A page in ChengOS is not a template file. It is the output of a workflow: UI nodes each
describe one region, ui/page_root assembles them, and the result is PageLayout JSON that the
runtime renders.
That indirection is the point. Because a page is workflow output, its content can be computed — a table's rows can come from a database query node in the same graph.
The composition
ui/top_nav ──────────┐
ui/left_sidebar ─────┤
ui/right_sidebar ────┼──► ui/page_root ──► PageLayout JSON ──► runtime renderer
ui/header ───────────┤
ui/content ──────────┤
ui/footer ───────────┘
Each region node emits a configuration object; ui/page_root collects them on named input ports
and produces the layout. Regions you do not connect simply do not appear.
| Node | Region |
|---|---|
ui/page_root | The skeleton — collects everything, outputs PageLayout |
ui/top_nav | Top navigation: logo, menus, action buttons |
ui/left_sidebar | Left navigation: menu items, icons, collapsible |
ui/right_sidebar | Right panel: properties, help, contextual info |
ui/header | Page header: title, breadcrumb, actions, tabs |
ui/content | Main content area: blocks |
ui/footer | Footer: copyright, links, social |
ui/route | A node in the route tree |
ui/rbac_guard | Access control — see Page Access Control |
Page root
ui/page_root carries the page's identity and global look:
| Field | Meaning |
|---|---|
site_id | Which site this page belongs to |
page_id | Page identifier used to load the config at runtime |
page_name | Human name, for docs and breadcrumbs |
theme | light (default) or dark |
layout_template | default, sidebar, topnav, or blank |
logo, favicon, primary_color | Branding, primary_color as hex, e.g. #1890ff |
output_base_dir | Archive root, default site_archive |
The archive path is {output_base_dir}/{site_id}/pages/{page_id}.json.
blank is the layout to reach for when you want no chrome at all — an embedded view or a
landing page.
Content blocks
ui/content holds a list of blocks. A block is deliberately generic:
{
"blockType": "table",
"title": "User list",
"span": 24,
"config": { "columns": [{ "key": "name", "title": "Name" }] }
}
| Field | Meaning |
|---|---|
blockType | card, table, form, chart, raw_html, iframe, tabs, grid, … |
title | Optional block heading |
span | Grid span, 1–24 |
config | Shape determined by the block type |
The grid is 24 columns: span: 24 is full width, span: 12 is half, span: 8 is a third. A
span of 0 is rejected rather than silently treated as full width.
ui/content also takes padding — a single number or a detailed
{top, right, bottom, left} — and a background colour.
config is untyped by design: the block type decides its shape, so adding a new block type in
the runtime does not require a backend schema change.
Routes
ui/route nodes form a tree: child routes connect into the parent's child port.
| Field | Meaning |
|---|---|
path | Static (users), dynamic (:id), or catch-all (*) |
name | Shown in menus and titles |
icon | Menu icon |
meta | SEO and extension data — title, requiresAuth, keywords, description |
page_workflow_id | The workflow that renders this route's page |
site_id | Root node only; children inherit it through the child port |
output_base_dir | Root node only, default site_archive |
A route binds a page workflow: visiting the route executes that workflow to render the page. A route may also override the layout template, taking priority over the page workflow's own default.
A route carries a status — ready (page workflow configured and usable), pending (not
configured yet), or failed (the page workflow errored) — so a half-built site is navigable and
its gaps are visible rather than producing blank pages.
Runtime versus build time
Two renderers consume the same PageLayout:
- Runtime —
chengflow-ui/src/runtime/renders PageLayout JSON into live pages and blocks: a miniature app-rendering engine with its own router, layouts, and block components. - Build time —
scripts/generate-pages.ts(pnpm gen:pages) executes the workflows against the backend, extractsPageLayoutand route trees, and writes them both topublic/site_archive/{siteId}/and to thepagesandroutestables. Configured throughscripts/pages-config.json.
This is why pnpm build needs the backend running: gen:pages is part of it, and it cannot
render pages without executing the workflows that produce them. See
Development Setup.
Static generation gives fast, cacheable pages for content that does not change per request; runtime rendering is for pages whose content depends on who is asking. You can mix them within one site.
Practical shape
Page workflow:
data source nodes ──► ui/content ──┐
ui/top_nav ────────────────────────┼──► ui/page_root
ui/left_sidebar ───────────────────┘
Route workflow:
ui/route (root, site_id) ──► ui/route (child, path ":id")
│
└── page_workflow_id → the page workflow above
Keep one page per workflow. A workflow that assembles three pages is harder to test than three workflows, and the route tree already gives you composition.
Next steps
- Publishing a Workflow as an App — getting a URL for this.
- Page Access Control & RBAC Guard — restricting routes.
- Table Workspace — a natural data source for table blocks.