Console extensions & site surfaces
Task-ordered recipes; the deep semantics live in Building feature plugins and the API reference.
Add a console page
- Export a
register*Console()that callsregisterConsoleExtensionwith anavItemsentry carrying a lazyComponent. - The shell serves it at
/[hostId]/<href>, wires breadcrumbs/header, resolves thefeatureFlagentitlement, and passesConsolePluginPageProps { hostId, entitled, org, permissions }. - Need data? Use
@aglyn/tenant-feature-instancehooks (useFirestoreCollection,usePluginConfig, …) — never console-app hooks. - First-party: add the register fn to your
plugins.config.jsonentry underconsoleand re-run the manifest codegen (the scaffolder did both if you used it).
Add a widget to a shell zone
Pick a zone from the catalog, then:
registerConsoleExtension({
pluginId: BUNDLE_ID,
displayName: 'My plugin',
widgets: [{ slot: 'dashboardFooter', widgetId: 'my-card', Component: MyCard }],
})
Your component receives the zone's documented props. Multiple plugins can target one zone; the shell renders all of them.
Wrap every console page (providers)
providers: [MyProvider] mounts around every console page once the
registry is populated — the marketplace plugin's AI-assist provider is the
reference. Providers receive the org billing doc as tenant.
Add a canvas component (Besigner + published sites)
Use defineUiFeatureBundle on your site surface — components get
schemas + presets and appear in the Besigner drawer. Component ids are
persisted in screen documents; never rename them. The editor and the
published site both suspend until your site surface registers, so the
canvas never renders unregistered components.
Add a site runtime
A runtime renders on every published page of workspaces that enable your plugin — overlay engines, analytics beacons, experiment runners:
registerSiteRuntime({ runtimeId: 'my-plugin', Component: MySiteRuntime })
Pair it with a server enricher (registerSitePageEnricher, /server)
that writes the props slice your runtime reads back from page. Enricher
errors are isolated — your slice drops, the page survives. Cross-plugin
wiring belongs in bootstrap<Surface>(), which runs after every plugin in
the batch has registered.
Troubleshooting
- Nav item missing: the plugin isn't enabled/released for the
workspace, or the register fn name in
plugins.config.jsondrifted (the loader logsmissing register fn). - Widget renders nowhere: check the slot name against the catalog —
unknown slots render only where an app mounts a custom
PluginWidgetSlot. - Canvas blank in the editor: your component registered on the
consolesurface instead ofsite— the editor gates loadsite.