MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.11

24. Web UI Server Components

MALDA includes native support for server-rendered UI using components, fragments, live updates, and the ui.* runtime.

Start here: Read 23. Web UI Overview to choose a UI model. This chapter covers two related but distinct server-side APIs. Do not pass HTML strings into ui.mount / ui.render, and do not treat a @PAGE return value as a ui.* tree (IDE UI1002).

24.1 Chapter Map

24.2 HTML Fragment Components

This section is the HTML fragment model: the server returns HTML strings (full page or a targeted fragment). It is additive and backward-compatible with existing @PAGE/@AIPAGE routes. For node trees and ui.mount, skip to 24.3.

Mental model: render on the server, mutate via action endpoints, optionally push updates through a live channel.

24.2.1 Minimal server-side example

component ItemBoard() {
    var items = componentStateGet("board", "items", []);
    var list = "";
    var i = 0;
    while (i < items.length) {
        list = list + "<li>" + items[i].title + "</li>";
        i = i + 1;
    }
    return "<h1>Items</h1><div id='item-list'><ul>" + list + "</ul></div>";
}

@ACTION("/items/add")
function addItem(body) {
    var items = componentStateGet("board", "items", []);
    items.append({"title": body.title, "status": "open"});
    componentStateSet("board", "items", items);
    componentLiveEmit("items", {"count": items.length}, "item-added");
    return componentFragment("item-list", "<ul><li>" + body.title + "</li></ul>");
}

@LIVE("/items/live")
function itemsLive() {
    return {"sse": true};
}

24.2.2 Tiny browser integration example

<form id="item-form">
  <input name="title" />
  <button type="submit">Add</button>
</form>
<div id="item-list"></div>
<script>
document.getElementById("item-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const fd = new FormData(e.target);
  const r = await fetch("/items/add", { method: "POST", body: fd });
  const html = await r.text();
  document.getElementById("item-list").innerHTML = html;
});

const es = new EventSource("/items/live");
es.onmessage = (evt) => console.log("live update", evt.data);
</script>

24.2.3 Fragment contract

When an @ACTION returns componentFragment(targetId, html), the HTTP response includes:

The built-in AJAX helper checks these headers and updates only the target element. If headers are missing, it falls back to full-page replacement.

24.2.4 LIVE channel subscription

Use query parameter ?channel=... on LIVE endpoints to scope events:

// Client subscribes only to item events
var es = new EventSource("/items/live?channel=items");

// Server emits only to channel "items"
componentLiveEmit("items", {"count": 8}, "item-added");

Multiple channels can be subscribed as comma-separated values, for example ?channel=items,alerts.

24.2.5 Server UI protocol envelopes

The server UI host uses versioned envelopes for mount/patch/event transport. Important fields include:

Clients should acknowledge processed envelopes and request resync when sequence gaps are detected.

Implementation notes: Runtime kernel (UiNode, diff, sessions), host endpoints, and env vars are documented in docs/ui-framework.md. This chapter stays focused on the language API.

24.2.6 Component API V2 direction

The server UI framework now includes a broader component set for forms, navigation, data, and feedback. Prefer the ui.* API in this chapter for component-centric server UI flows, and keep @PAGE/@AIPAGE for route-first HTML generation patterns.

Advanced controls include ui.dataGrid (sorting/filtering/pagination, row selection, viewport virtualization events, drag/drop hooks) and ui.treeView (hierarchical nodes, expand/collapse, keyboard-friendly selection, optional lazy loading). In both controls, keep business state server-side and use client-side transient state for scroll/viewport responsiveness.

24.2.7 Migration guide: @PAGE to component

  1. Keep your existing @PAGE route as-is for compatibility.
  2. Move reusable rendering logic into a new component function.
  3. Add @ACTION endpoints for form and button interactions.
  4. Use componentFragment(...) to return only the affected HTML block.
  5. Add @LIVE + componentLiveEmit(...) if the page needs push updates.

24.3 Web UI Helper API Reference

This section is the language API for both fragment helpers (componentFragment, templates, CRUD HTML) and server-driven ui.* trees (ui.mount, controls, event loop). Shared state helpers (componentState* / ui.state) apply to both.

24.3.1 Server component helper functions

var html = renderTemplate(
    "<h1>{{title}}</h1><p>{{count}} items</p>",
    {"title": "Items", "count": 4}
);

@ACTION("/items/add")
function addItem(body) {
    return componentFragment("item-list", "<ul><li>" + body.title + "</li></ul>");
}

componentLiveEmit("items", {"count": 7}, "item-added");

24.3.1.1 UI-native template workflow (transparent UX)

For larger apps, use the UI template helpers to keep HTML in external files and MALDA logic in code:

var base = "templates/";

var sidebar = ui.template(base + "sidebar.html", {
    "overviewClass": "nav-link active",
    "itemsClass": "nav-link"
});

var rowsHtml = ui.renderList(items, base + "item_row.html", "item");

var body = ui.partial(base + "items_body.html", {
    "rows": rowsHtml
});

return ui.layout(
    base + "layout.html",
    {
        "sidebar": sidebar,
        "pageBody": body,
        "pageScript": "initItems();"
    },
    {
        "pageTitle": "Items",
        "appCssPublicPath": "/static/app.css"
    }
);

Template syntax:

<ul>
{{#each items as item}}
  {{#if item.open}}
    <li>{{item.title}} - {{{item.badgeHtml}}}</li>
  {{/if}}
{{/each}}
</ul>

Compatibility: pass {"compatRaw": true} to ui.template(..., ..., options) to preserve legacy raw behavior for {{key}} during migration.

Caching: template files are cached by default. Pass {"cache": false} as the third argument to ui.template for development-time reload behavior.

Recommended structure: keep template files under a project-local templates/ directory (for example, templates/layout.html) and reference them by path. Runnable Web samples live under Examples/Web/ (for example ui_form_workflow.malda and ui_controls_showcase_minimal.malda).

24.3.1.2 Schema-driven CRUD helper pattern

For CRUD pages that share the same UI skeleton (filters, add/edit dialogs, list controls), use the built-in schema pipeline:

Default behavior: ui.crudControls resolves entity_controls.html under the schema templateBasePath (default templates) when no template override is provided.

var schema = itemsCrudSchema; // data only
var sessionId = queryParams["session"] == null || queryParams["session"] == "" ? schema.sessionDefault : queryParams["session"];
var lookups = {"owners": allOwners(), "tags": listTags()};
var controlsHtml = ui.crudControls(schema, sessionId, queryParams, lookups);
var normalized = ui.crudSchema(schema, {
  "templateBasePath": "templates",
  "controlsTemplatePath": "templates/entity_controls.html"
});
var controlsHtml = ui.crudControls(normalized, sessionId, queryParams, lookups);

Overrides: pass {"templatePath": "...", "templateBasePath": "...", "cache": false} in ui.crudControls(..., options) when you need a different controls template, base path, or cache behavior.

Migration note: if your app currently normalizes CRUD schema fields manually, migrate that logic to ui.crudSchema to keep schema defaults consistent across apps.

24.3.1.3 CRUD schema reference

The object passed to ui.crudSchema, ui.crudModel, and ui.crudControls may contain the following properties. Values are copied into the model used to render the controls template; missing properties get defaults as described.

Top-level schema properties

PropertyTypeDescription / default
entityPluralLowerstringEntity name plural, e.g. "items". Passed to the controls template.
entitySingularLowerstringEntity name singular, e.g. "item". Used by ui.crudSchema to derive default openAddLabel / openEditLabel when omitted (default "item").
sessionDefaultstringDefault session id when sessionId is not passed to ui.crudControls / ui.crudModel. Default "default".
templateBasePathstringBase directory for dialog and lookup option templates. ui.crudSchema default: templates (or from defaults.templateBasePath).
controlsTemplatePathstringPath to the entity controls template (filters + buttons + grid host). Default: templateBasePath + "/entity_controls.html". Fallback: controlsTemplate.
listActionstringURL or path for the list page, e.g. "/web/items". Passed to the controls template.
filterGridColumnsstringCSS grid columns for the filter bar, e.g. "2fr 1fr auto". Passed to the controls template.
openAddButtonIdstringDOM id of the button that opens the add dialog, e.g. "openItemAddDialog".
openEditButtonIdstringDOM id of the button that opens the edit dialog, e.g. "openItemEditDialog".
openAddLabelstringLabel for the add action. ui.crudSchema default: "Add " + entitySingularLower (or from defaults.openAddLabel).
openEditLabelstringLabel for the edit action. ui.crudSchema default: "Edit selected " + entitySingularLower (or from defaults.openEditLabel).
addDialogTemplatestringTemplate filename for the add dialog HTML (resolved under templateBasePath).
editDialogTemplatestringTemplate filename for the edit dialog HTML (resolved under templateBasePath).
dialogScriptTemplatestringTemplate filename for the dialog script (resolved under templateBasePath).
filterDefsarrayList of filter definitions (see below). ui.crudSchema coerces non-array to [].
dialogLookupOptionsarrayList of lookup bindings for add/edit dialogs (see below). ui.crudSchema coerces non-array to []. Each source must exist in the lookups object passed to ui.crudControls / ui.crudModel or dropdowns will be empty.
gridDescriptionstringOptional. Description for the grid host; used by app layouts for accessibility or layout.
envelopeApiPathstringOptional. API path for the UI envelope; used by app layouts that mount the grid via ui.mountEnvelope.

filterDefs — each element is an object:

PropertyTypeDescription
kindstring"input" (text) or "select" (dropdown).
namestringQuery parameter name; required. Used to read/write the filter value from queryParams.
placeholderstringPlaceholder text for kind === "input".
defaultValuestringDefault value when the query param is missing.
optionsarrayFor kind === "select". Each item: {"value": "...", "label": "..."}. Rendered as <option>s with selected set from current query.

dialogLookupOptions — each element is an object that binds one lookup array to a key in the dialog model (e.g. for dropdown options in add/edit forms):

PropertyTypeDescription
keystringName under which the rendered HTML is set in the dialog model (e.g. "customerOptions"). The add/edit dialog template uses this to inject option HTML.
sourcestringKey in the lookups object passed to ui.crudControls / ui.crudModel (e.g. "customers", "agents"). Required.
rendererstringOptional. Built-in hint: "customerOptions" or "agentOptions" supply default templatePath and itemName when templatePath is omitted.
templatePathstringPath to the template that renders one option row (resolved with templateBasePath if relative). Required unless renderer is one of the built-ins.
itemNamestringName of the loop variable in that template (default "item"). For renderer === "customerOptions" / "agentOptions" the default becomes "customer" / "agent".

Template resolution: When calling ui.crudControls(schema, ...), the controls template can be overridden per call via options.templatePath. The base path for dialog/lookup templates can be overridden via options.templateBasePath. Schema values templateBasePath and controlsTemplatePath (or controlsTemplate) are used when options do not provide overrides.

24.3.2 Server component state helpers

var items = componentStateGet("board", "items", []);
items.append({"title": "Login issue"});
componentStateSet("board", "items", items);
print(componentStateObject("board"));
componentStateClear("board");

// Tenant/session scoping (optional)
componentStateSet("board", "count", 1, "tenantA");
componentStateSet("board", "count", 2, "tenantB");

// Safety policy tuning (defaults are conservative)
componentStateConfigure(512, 128, 1800000);

// Process-lifetime shared data (catalog, boot config): pin after write
componentStateSet("brain", "catalog", catalog, "ask-shared");
componentStatePin("brain", "ask-shared");

24.3.3 Server UI runtime and lifecycle APIs

ui.configure supports maxPatchCount, maxPayloadBytes, maxEventQueueDepth, and sessionTtlMs.

var envelope = ui.mountEnvelope(rootNode, "app-session", {
  "maxPatchCount": 1024,
  "maxEventQueueDepth": 1024,
  "sessionTtlMs": 1800000
});
print(envelope.mount.type);
print(envelope.snapshot.version);
print(envelope.resync.type);
// source can be request object, body object, or query/body map containing "session"
var sessionId = ui.sessionId(req, "items-ui");
return ui.redirectWithSession("/web/items", body, "items-ui");

Lifecycle events are emitted through ui.pullEvent() as type = "lifecycle" payloads. Typical ordering is:

Subscribe by calling the matching helper with the component id (and optional session). That registers the hook; the payload still arrives on ui.pullEvent as type = "lifecycle". Offline showcase: Examples/Web/ui_counter_dashboard.malda.

ui.onMount("CounterRoot", sessionId);
ui.onUpdate("CounterRoot", sessionId);
ui.onUnmount("CounterRoot", sessionId);

24.3.3.1 How a ui.* session is hosted

A program that calls ui.mount or ui.mountEnvelope can start the embedded UIHost (CLI and Desktop). That host is not HttpServer / @PAGE — those stay in 25. HttpServer & HTML UI Generation.

To try a tree without a browser, run the offline loop below (or Examples/Web/ui_event_loop.malda). AI-generated trees use ui.generate — see 25.8.5.

24.3.3.2 Event loop (mount → pull → render)

After each client event: dispatchEvent (the host enqueues; examples may simulate) → pullEvent → update state → rebuild the tree → ui.render. Skipping pullEvent leaves the queue full and the UI looks stuck (IDE UI1001).

var sessionId = "rm-event-loop";
var count = ui.state("LoopRoot", "count", 0, sessionId);

function buildView(value) {
    return ui.column(
        {"componentId": "LoopRoot"},
        [
            ui.text({"value": "count=" + string(value)}),
            ui.button({"label": "Inc", "onClick": "increment"})
        ]
    );
}

var mounted = ui.mount(buildView(count), sessionId);
print(mounted.type);

ui.dispatchEvent(
    {"type": "click", "targetPath": "/1/", "payload": {"action": "increment"}},
    sessionId
);

var evt = ui.pullEvent(sessionId);
if (evt != null) {
    count = count + 1;
    ui.setState("LoopRoot", "count", count, sessionId);
}

var patched = ui.render(buildView(count), sessionId);
print(patched.type);
print(count);

24.3.4 UI control catalog

UI controls follow a consistent signature: ui.control(props, children?, key?).

ui.datePicker(props, children?, key?) renders a native date (or date+time) input. Props: name, value, defaultValue, placeholder, disabled, includeTime. Set includeTime to true for a date-and-time picker. Use ISO date strings for value/defaultValue: YYYY-MM-DD or YYYY-MM-DDTHH:mm. Events: onChange, onInput.

24.3.4.1 Styling controls

Use a hybrid styling approach for Web UI controls: apply reusable design rules with className, and apply per-instance/dynamic tweaks with style.

var card = ui.panel({
    "className": "dashboard-card",
    "style": {"padding": "12px", "borderRadius": "10px", "backgroundColor": "#ffffff"}
}, [
    ui.text({"value": "Hybrid styling example"})
]);

24.3.5 Advanced data controls

ui.dataGrid(props, children?, key?) supports sorting/filtering/pagination, row selection, optional virtualization, and drag/drop row interaction events.

Sorting by clicking column headers works with sortable columns. If onSort is provided, the grid emits sort events for server-side/state-managed sorting; otherwise the client applies local sorting using the current sort state.

ui.treeView(props, children?, key?) provides hierarchical navigation with expand/collapse, keyboard-friendly selection, optional lazy loading, and drag/drop node interaction events.

When lazy is enabled, expanding a node without loaded children should trigger server-side loading (typically through onLoadChildren) and then re-render with the returned children.

var grid = ui.dataGrid({
    "columns": [{"key": "name", "title": "Name", "sortable": true}],
    "rows": [{"id": 1, "name": "Andrea"}],
    "rowKey": "id",
    "selectionMode": "single",
    "onSort": "sortUsers",
    "onSelectionChange": "selectUser",
    "onDrop": "moveUserRow",
    "virtualize": true,
    "rowHeight": 32,
    "overscan": 4
});

var tree = ui.treeView({
    "nodes": [{"id": "root", "label": "Root", "hasChildren": true, "childrenLoaded": false}],
    "lazy": true,
    "expandedKeys": ["root"],
    "onNodeToggle": "toggleNode",
    "onNodeSelect": "selectNode",
    "onLoadChildren": "loadTreeChildren",
    "onDrop": "moveTreeNode"
});

24.3.6 Control props reference

Contracts below match UiControlSpecRegistry. Every control also accepts the shared props className, id, role, ariaLabel, componentId, key, disabled, and style. Event props are action-name strings in the emitted payload — keep those names stable across renders.

Layout (ui.row, ui.column, ui.stack, ui.spacer, ui.panel, ui.field, ui.drawer, ui.tabs, ui.accordion, ui.breadcrumbs, ui.list, ui.table, ui.emptyState, ui.badge, ui.alert, ui.progress, ui.skeleton, ui.spinner, ui.errorBoundary, ui.slot, ui.when, ui.choose, ui.each): shared props only. ui.tabs commonly uses a custom value (active tab key) with ui.panel children that have a key.

ControlExtra propsEvents
ui.text / ui.headingvalue (required)
ui.imagesrc, alt, width, height
ui.iconname, size
ui.buttonlabel, variant (primary|secondary|danger)onClick
ui.textFieldname, value, defaultValue, placeholderonChange, onInput
ui.textAreasame as textField, plus rowsonChange, onInput
ui.checkbox / ui.switchname, checked, defaultChecked, labelonChange
ui.select / ui.radioGroupname, value, defaultValue, options (array of {"label","value"})onChange
ui.slidername, value, min, max, steponChange, onInput
ui.datePickername, value, defaultValue, placeholder, includeTime (ISO YYYY-MM-DD or YYYY-MM-DDTHH:mm)onChange, onInput
ui.formmethod, action (plus shared componentId)onSubmit
ui.modalopen, titleonClose
ui.toastopen, message, variant (success|warning|error|info)onClose
ui.paginatorpage, pageSize, totalItemsonChange
ui.dataGridsee 24.3.5see 24.3.5
ui.treeViewsee 24.3.5see 24.3.5

Unknown custom attributes are accepted (permissive mode). Event names that are not in the table throw at tree construction.

24.3.7 Control usage examples

Implementation-ready snippets for common form and list patterns:

// Button with primary variant and action
ui.button({"label": "Save", "variant": "primary", "onClick": "saveItem"})

// Form with submit action and validation
ui.form({"componentId": "ItemForm", "onSubmit": "submitItem"}, [
    ui.field({}, [ui.text({"value": "Title"}), ui.textField({"name": "title", "required": true})]),
    ui.button({"label": "Create", "variant": "primary", "onClick": "submit"})
])

// Modal with close handler
ui.modal({"open": true, "title": "Edit item", "onClose": "closeEditDialog"}, [
    ui.text({"value": "Form content here"})
])

// Tabs with active state
ui.tabs({"value": "overview"}, [
    ui.panel({"key": "overview"}, [ui.text({"value": "Overview content"})]),
    ui.panel({"key": "details"}, [ui.text({"value": "Details content"})])
])

// DataGrid with row click and selection
ui.dataGrid({
    "columns": [{"key": "name", "title": "Name", "sortable": true}],
    "rows": [{"id": 1, "name": "Alice"}],
    "rowKey": "id",
    "selectionMode": "single",
    "onRowClick": "openDetail",
    "onSelectionChange": "selectRow"
})

// TreeView with lazy loading
ui.treeView({
    "nodes": [{"id": "r", "label": "Root", "hasChildren": true, "childrenLoaded": false}],
    "lazy": true,
    "expandedKeys": ["r"],
    "onLoadChildren": "loadChildren",
    "onNodeSelect": "selectNode"
})

// Toast and error boundary
ui.toast({"open": true, "message": "Saved", "variant": "success", "onClose": "dismissToast"})
ui.errorBoundary({}, [ui.text({"value": "Something went wrong. Please try again."})])

24.3.8 Event handling patterns

Wire user interactions to server actions using consistent patterns:

Action names are strings your server-side event loop maps to behavior. In server UI flows, process the pulled event payload, update state, and then emit fragments or re-render.

// Client: form submits; server returns fragment or JSON
ui.form({"onSubmit": "submitItem"}, [
    ui.textField({"name": "title", "onChange": "validateTitle"}),
    ui.button({"label": "Submit"})
])

// Server: process events from the queue
ui.dispatchEvent({"type": "submit", "targetPath": "/", "payload": {"action": "submitItem", "title": "Bug report"}}, sessionId);
var evt = ui.pullEvent(sessionId);
if (evt != null && evt.payload != null && evt.payload.action == "submitItem") {
    // validate/persist, then ui.setState(...) or return componentFragment(...)
}

For template-based forms, use method="post" and action="/path/..." with the built-in AJAX helper. See 29. Full-Stack Development with MALDA for end-to-end patterns.

24.3.9 Form validation patterns and error rendering

Validate input on the server before persisting. Return validation errors in the response and render them next to fields:

@ACTION("/items/add")
function addItem(body) {
    var errors = [];
    if (body.title == null || body.title == "") {
        errors.append("Title is required");
    }
    if (body.email != null && indexOf(body.email, "@") < 0) {
        errors.append("Invalid email");
    }
    if (errors.length > 0) {
        var errHtml = "<ul class='error-list'>";
        var i = 0;
        while (i < errors.length) {
            errHtml = errHtml + "<li>" + errors[i] + "</li>";
            i = i + 1;
        }
        errHtml = errHtml + "</ul>";
        return componentFragment("item-form-errors", errHtml);
    }
    // persist and return success fragment
    return componentFragment("item-list", "<ul><li>" + body.title + "</li></ul>");
}

Security: Always escape user-supplied content before embedding in HTML. Use {{key}} (escaped interpolation) in templates, or use a trusted escaping helper for raw string concatenation. Never use {{{key}}} for user input.

24.3.10 Composition patterns

Build reusable UI from smaller pieces using ui.slot, ui.withSlot, ui.when, ui.choose, and ui.each:

var card = ui.panel({}, [ui.slot({"name": "title"}), ui.slot({"name": "body"})]);
var cardWithTitle = ui.withSlot(card, "title", ui.heading({"value": "Card Title"}));
var cardComplete = ui.withSlot(cardWithTitle, "body", ui.text({"value": "Body content"}));

var isOpen = true;
var status = "success";
var statusBadge = ui.when(isOpen, ui.badge({"value": "Open"}), ui.badge({"value": "Closed"}));
var alertByType = ui.choose(status, {"success": ui.alert({"variant": "success"}, [ui.text({"value": "OK"})])}, ui.alert({"variant": "info"}, [ui.text({"value": "Unknown"})]));
var labels = ui.each(["a", "b", "c"]);
var list = ui.column({}, labels);

24.3.11 State management patterns

HttpServer request handlers run in an isolated interpreter, so top-level variables are not shared across requests. Use the component state store (via ui.* or componentState*) for cross-request data.

Rule of thumb: peek with ui.getState when reading; use ui.state only when you intentionally want get-or-create. Pin shared brain/config scopes; keep conversation scopes under normal TTL/LRU. Persist long-term invariants in files or a database when the process may restart. Offline golden: Examples/Web/ui_state_lifecycle.malda — also docs/ui-framework.md § State model.

// Peek (safe): missing key does not poison the store
var catalog = ui.getState("AskStore", "catalog", null, sharedScope);

// Get-or-create (OK for lists you always want initialized)
var items = ui.state("board", "items", [], convScope);
items.append({"title": "New", "status": "open"});
ui.setState("board", "items", items, convScope);

// Process-lifetime shared meta
ui.setState("AskStore", "session", meta, sharedScope);
ui.pinState("AskStore", sharedScope);

24.3.12 Routing and query param patterns

For master-detail and multi-page flows, use query parameters:

@PAGE("/web/items")
function itemsPage(req) {
    var queryParams = req.query == null ? {} : req.query;
    var sessionId = ui.sessionId(req, "items-ui");
    var schema = itemsCrudSchema;
    var lookups = {"owners": listOwners(), "tags": listTags()};
    var controlsHtml = ui.crudControls(schema, sessionId, queryParams, lookups);
    return appLayoutHtml("items", "Items", controlsHtml, "");
}

For REST API calls from the client, use fetch or RestClient; see 28. REST Web Client for server-side HTTP calls and 27. REST API Server for API design.

24.3.13 Error and security notes

24.3.14 Styling guidance

Keep layout and CSS conventions consistent across pages:

var body = ui.layout(
    "templates/layout.html",
    {"sidebar": sidebarHtml, "pageBody": bodyHtml},
    {"pageTitle": "Items", "appCssPublicPath": "/static/app.css"}
);

24.3.15 Common pitfalls and debugging

24.4 Request-to-Render Lifecycle

  1. Browser sends request to @PAGE, @AIPAGE, @ACTION, or @GET/@POST route.
  2. Server resolves route and builds HTML or a component fragment.
  3. For component flows, server updates state (componentState* or ui.state/setState).
  4. Server returns either a full document, fragment response, or protocol patch envelope.
  5. Optional: SSE subscribers receive componentLiveEmit/ui.invalidate messages for live updates.

For the route-first parts of this lifecycle, including HttpServer, @PAGE, and AI-generated HTML, see HttpServer & HTML UI Generation.

24.5 Production Hardening Checklist

24.6 Testing and Troubleshooting

24.7 Related Chapters and Examples

Offline and hosted samples under Examples/Web/:

See Also