Serve and embed a spaday app¶
This guide shows you how to deliver a spaday UI to a browser — from “spaday runs the whole app” down to “spaday is one node on a page you already own.” Each rung hands more control to the host, and every rung takes the same generation options. (For keeping the UI in sync with a server-side model, see Sync over transports; for a notebook, see Use in a notebook.)
There is no hand-written HTML: spaday generates the bootstrap page from your Python description.
Rung |
spaday owns |
Seam |
|---|---|---|
No HTML |
the whole app |
|
Some HTML |
a sub-path of your app |
|
Full custom HTML |
one node in your page |
|
Notebook |
a cell’s widget |
|
page is a built component or a zero-arg callable returning one — a callable is re-rendered per
request, so the tree can reflect current state.
Serve a whole app¶
serve creates a Starlette app and mounts your page on it: it generates the bootstrap HTML, hosts the
tree at /tree.json, and serves core JS at /js. Pull a component library into <head> with
packages=, add your own routes with routes=, and run lifetime coroutines with background=:
import uvicorn
from spaday.backends.starlette import serve
from spaday_webawesome import WaButton
app = serve(
lambda: WaButton(variant="brand").text("Hi"),
packages=["webawesome"], # pull WebAwesome's styles + catalog into <head>
head="<style>body{margin:2rem}</style>",
title="my app",
)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
Component integrations are peer packages: spaday-webawesome, spaday-lightweight-charts,
spaday-regular-layout, spaday-regular-table, and spaday-perspective all use packages= (see
below). serve is the one-line happy path — it is mount onto a fresh app, so drop to mount when the
app is yours.
In a source checkout it serves built assets from js/; from a wheel it automatically serves packaged
spaday/extension assets. Use layout="source" or layout="installed" only to override detection, such
as when supplying a matching custom js= directory.
Install an external component package¶
External integrations use one ComponentPackage descriptor for both <head> tags and static routes.
An application can pass the descriptor directly or select it by Python path:
from spaday_trees import package as trees
app = serve(page, packages=[trees])
app = serve(page, packages=["spaday_trees:package"])
An integration package defines that descriptor beside its built assets:
from pathlib import Path
from spaday import ComponentPackage
package = ComponentPackage(
name="trees",
assets_dir=Path(__file__).parent / "extension",
assets=(("css", "trees.css"), ("js", "trees.js")),
)
To make the short form packages=["trees"] available, publish the same object as a Python packaging
entry point:
[project.entry-points."spaday.component_packages"]
trees = "spaday_trees:package"
Entry-point packages are opt-in: spaday loads only names selected by the application, never every
installed integration. All four backends serve each selected descriptor’s assets_dir at
{prefix}/components/{name}/; bootstrap emits its CSS and module-script URLs from the same assets
list. This registration is host-side only: component tags and props already cross the generic spaday
tree, so an integration needs no Rust plugin.
Embed in an existing app¶
mount adds spaday’s routes to an app you already have, under a prefix — it touches nothing else.
The page, tree, /js, and your supplied routes are all prefixed, so a wired panel’s generated
websocket URL lines up with its endpoint (pass the unprefixed path; mount adds the prefix). mount
only adds routes — the host owns the app’s lifespan, so run any background work in your own lifespan:
from starlette.applications import Starlette
from starlette.routing import Route
from spaday.backends.starlette import mount
app = Starlette(routes=[Route("/", my_own_homepage)]) # your app, your routes
mount(app, page, prefix="/panel", packages=["webawesome"]) # spaday lives only under /panel
Backends ship for Starlette/FastAPI, aiohttp, Flask, and Tornado — import serve/mount
from spaday.backends.<name>. They are thin glue over the framework-agnostic generator.
Drop into a host page¶
When the host owns the entire HTML page (its own markup, CSS, bundler), emit spaday as a fragment —
just the bundle tags + the mounting <script>, with no document — and splice it into a node the host
provides. The host serves the tree, core /js, and selected package assets itself:
from starlette.responses import HTMLResponse, Response
from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles
from spaday_webawesome import package as webawesome
from spaday.bootstrap import bootstrap, bundles_dir, tree_json
async def home(_request):
fragment = bootstrap(fragment=True, target="#spaday-root", packages=[webawesome])
return HTMLResponse(f"<!doctype html>… <div id='spaday-root'></div> {fragment} …")
routes = [
Route("/", home),
Route("/tree.json", lambda _r: Response(tree_json(page), media_type="application/json")),
Mount("/js", StaticFiles(directory=bundles_dir())),
Mount("/components/webawesome", StaticFiles(directory=webawesome.assets_dir)),
]
The mounting script is inline, so a host with a strict script-src Content-Security-Policy passes a
per-request nonce — it stamps the generated <script>/<link> tags so the policy can allow them:
fragment = bootstrap(fragment=True, target="#spaday-root", packages=[webawesome], nonce=request_nonce)
# ...and set `Content-Security-Policy: script-src 'self' 'nonce-<request_nonce>' 'wasm-unsafe-eval'`
Seed reactive state without a server¶
For client-side reactive UI (two-way bindings, field actions) that needs no server model, seed a local
signal store with store= — the page mounts with that state, no wire required:
app = serve(page, store={"dark": False, "view": "list"}) # the tree's bindings read/write these fields
Connect a live model¶
To keep the UI in sync with a server-side model, add a wire: wire="transports" for one model, or a list
of Wire specs for several. The generated page opens the websocket(s) and binds them to
the store; you supply the websocket route and run autosync. See Sync over transports.
import transports
from starlette.routing import WebSocketRoute
app = serve(
page,
wire="transports",
routes=[WebSocketRoute("/ws", transports.ws_endpoint(server))],
background=[transports.autosync(server)],
)
The route contract¶
Whatever rung you pick, the generated page expects the host to serve these paths ({base} is the
prefix, empty by default) — serve/mount wire them for you:
Path |
Serves |
|---|---|
|
the bootstrap HTML ( |
|
the authored tree ( |
|
core assets under |
|
assets for each selected |
|
a transports endpoint (only when wired) |