Static assets and fingerprinting

Ship CSS, JavaScript, and images with content-hashed URLs so browsers can cache them across deploys. Use Assets for that tree. Use Files when the path on disk is the path in the URL (uploads, generated files). Both live on stario. stario.staticassets is obsolete.

This page is the recipe. For the HTTP contract, see Files and assets. For where files live in a larger app, see Structuring apps.

Assets at module level

Build Assets when the module loads. href() hashes that file and pins the digest. It does not keep file contents in memory.

The static directory must exist when assets.py is imported. Finish writing assets before the first href() or attach(). A missing path raises StarioError. A file that changes while hashing raises StarioRuntime. Restart after you replace an Assets file.

python
from pathlib import Path
 
from stario import Assets
 
# Single-file app: static/ sits next to main.py
ASSETS = Assets(Path(__file__).resolve().parent / "static")
STYLE_CSS = ASSETS.href("css/style.css")
APP_JS = ASSETS.href("js/app.js")

In a larger app, static/ sits next to app/, so app/assets.py walks one directory further:

python
PROJECT_ROOT = Path(__file__).resolve().parents[1]
ASSETS = Assets(PROJECT_ROOT / "static")

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.

attach in bootstrap

await attach(app) registers GET and HEAD, then loads the rest of the tree (hash, hold small files, precompress). Wrap it in a span step so the returned stats land on the startup trace:

python
from stario import App, Route, Span
 
from app.assets import ASSETS
 
 
async def bootstrap(app: App, span: Span):
    with span.step("static_assets") as s:
        s.attrs(await ASSETS.attach(app))
 
    app.add(Route("GET /"), index)
    yield

Assets serves fingerprinted files with immutable cache headers. Small files are held in memory and precompressed (br, zstd, gzip) by default. Larger files stream from disk and support Range (uncompressed). Logical paths without a digest get 307 to the hashed URL.

register() and load() stay available when you must split the work. attach() is the bootstrap call. Order relative to other routes only matters for app.use on the same prefix.

Optional tuning: url_prefix= and content_types= on construction; precompress= and cache_control= on attach(). See Files and assets.

Files for a live tree

Files uses the live path. Default prefix is /data. GET is live after attach — a new file on disk is served. Files does not precompress unless you pass precompress=. Do not use Assets for visitor uploads.

python
from stario import Files
 
UPLOADS = Files("./uploads", "/data")
 
 
async def bootstrap(app: App, span: Span):
    await UPLOADS.attach(app)
    yield

Writing the bytes is still your handler. See User uploads and storage.

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 Route constants and .href() — see Structuring apps.

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 attach() runs in tests.