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.

python
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:

python
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)
    yield

StaticAssets 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:

python
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:

python
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 == 200

Use the same bootstrap as production so StaticAssets.register runs in tests.