Static assets and fingerprinting
Ship CSS, JavaScript, and images with content-hashed URLs so browsers cache aggressively while you deploy often. Stario splits the work in two: scan and fingerprint at import time (AssetManifest), then serve and optionally pre-compress during bootstrap (StaticAssets).
This page is the recipe. For where files live in a larger app, see Structuring larger applications.
AssetManifest at module level
Build the manifest once when the module loads. Hashing reads each file’s bytes to compute xxHash64 digests but does not keep contents in memory, so module-level constants stay cheap.
Operational constraints: the static directory must exist when assets.py is imported. Finish writing assets before constructing AssetManifest (files that change mid-scan raise StarioError). StaticAssets verifies size and mtime at bootstrap — if files change after the manifest was built, restart or rebuild the manifest. ASSETS.href("missing.css") raises StarioError on typos.
from pathlib import Path from stario import AssetManifest ASSETS = AssetManifest(Path(__file__).resolve().parent / "static")STYLE_CSS = ASSETS.href("css/style.css")APP_JS = ASSETS.href("js/app.js")href(logical_path) returns the public URL (by default under /static/… with a fingerprint segment). Use those strings in views—no url_for and no App instance at import time.
Hidden files and dot-directories are skipped unless you pass include_hidden=True. Symlinks are skipped unless follow_symlinks=True.
StaticAssets in bootstrap
Registration belongs in bootstrap, wrapped in a span step so telemetry records what was registered:
from stario import App, Span, StaticAssets from app.assets import ASSETS async def bootstrap(app: App, span: Span): with span.step("static_assets") as s: static = StaticAssets(ASSETS) s.attrs(static.stats) static.register(app) app.get("/", index) yieldStaticAssets serves fingerprinted files with immutable cache headers and optional bootstrap pre-compression for small files (≤ cache_max_size, default 1 MiB). Larger files stream from disk and support Range (uncompressed). Logical paths without a digest get 307 to the canonical URL. GET and HEAD are registered. Call static.register(app) after construction; order relative to routes only matters for app.use on the same prefix.
Optional tuning: url_prefix= on AssetManifest, precompress= and content_types= on StaticAssets. See Static assets.
ASSETS.href in views
Resolve URLs before you build HTML. Pass plain strings into view functions:
from stario.markup import html as h from app.assets import APP_JS, STYLE_CSS def layout(*children): return h.HtmlDocument( {"lang": "en"}, h.Head( h.Link({"rel": "stylesheet", "href": STYLE_CSS}), h.Script({"type": "module", "src": APP_JS}), ), h.Body(*children), )In handlers, use the same module-level constants or call ASSETS.href("…") when the path depends on runtime data.
For links to application routes, use UrlPath constants and .href()—see Structuring larger applications.
Tests
Assert fingerprinted URLs resolve through the running app:
from app.assets import ASSETS async def test_static_asset(client): url = ASSETS.href("css/style.css") assert url.startswith("/static/") r = await client.get(url) assert r.status_code == 200Use the same bootstrap as production so StaticAssets.register runs in tests.
Related
Static assets reference —
AssetManifest,StaticAssetsStructuring larger applications —
app/assets.pyconventionRuntime —
AssetManifest,StaticAssets,StaticAssets.stats