Static assets
August 9, 2026 · View on GitHub
GET /static/{path} serves the assets that every function in a space shares. It
is an application route, like /doc, not a per-function route:
there is one per space, and every page in that space pulls from it, so they
share the download and the cache. That is why static is a reserved slug.
Two sources, in order
func_to_web/static, FuncToWeb's own assets;pytypehintweb.STATIC, the widget layer's assets.
The first one that exists is served, so if the same name existed in both, FuncToWeb would win.
FuncToWeb's own assets are six files and one folder:
| Asset | What it is |
|---|---|
page.js | Builds a function page from the plan embedded in the markup, and runs it |
upload.js | The upload that precedes /invoke, with its progress modal |
output.js | The rendering of the outputs with their copy and download buttons |
emit.js | What the page announces to whoever embeds it |
page.css | The style of that page |
sdk.js | The helpers a frontend of your own calls the space with |
icons/ | The five SVGs the outputs use |
sdk.js is the one no function page asks for: it is served because it is an
asset like any other, not because the page graph reaches it.
Everything else comes from pytypehintweb (form.js, widgets.css, its own
icons/, and the modules they import), served from wherever the package is
installed, with nothing copied or repackaged.
A tree, not a list
{path} can contain subdirectories, because the style sheets reference their
icons relatively:
/{prefix}/static/widgets.css → url("./icons/trash.svg")
/{prefix}/static/icons/trash.svg
What decides what gets served is resolution, not pattern matching: the path is
resolved, and it must still land inside one of the two directories and must be a
file. A traversal (../router.py and its encoded variants), an absolute path, a
symlink pointing outside, or a directory all get a 404, with no distinction
between "not there" and "not allowed".
The content type is decided here, not guessed:
.js text/javascript
.css text/css
.svg image/svg+xml
Those are the three extensions the two libraries ship. Why the type is decided here rather than guessed is a deliberate choice → design/frontend.md.
Icons
All SVGs are files, in both layers, and FuncToWeb's own are drawn with
mask-image from page.css, so they keep currentColor and follow the theme
like any other text. The URL is relative to the style sheet, so they work under
any prefix. What that rules out, and why →
design/frontend.md.
No absolute paths on a function page
Icons are not a special case. Everything a function page requests, it
requests relatively: ../static, ../upload, ../returns/…, ./icons/*.svg.
That way the application prefix is inherited from the page's own URL, and nothing the
page loads has to know where the space was mounted. See router.md.
The index is no exception either. It is part of
app_of(), served at the / of the space, and it is built with "." as its
prefix, so its three prefixed references — the style sheet, /doc, and the
src of each iframe — come out as ./static/widgets.css, ./doc and
./{slug}/. They resolve against the URL of the index itself, and a request
for the mount point without its trailing slash is redirected to it first, so
the base always ends in / and every reference lands inside the prefix. The
index therefore travels with the application to whatever mount it is given,
exactly like a function page does.
Theme
page.css carries no widget palette: it relies on the --pth-* tokens from
widgets.css, which exist only inside a .pth-root. That is why the
<body> of every page is that root, and the whole page (header, button,
outputs, and upload modal) reads the same tokens by inheritance and paints a
single surface.
The server writes data-pth-theme on the <html> element from the theme of
app_of(), as part of the initial markup, and
widgets.css resolves the three values in pure CSS: there is nothing to run in
<head> and no preference to restore
(design/router.md).
Almost all the color comes from the widgets.css palette. Two --ftw-*
exceptions remain, plus --ftw-color-scheme, which is not a color: why there
are exactly those and not one more is a rule worth reading →
design/frontend.md.
Caching
Responses carry Cache-Control: public, max-age=3600 and an ETag computed
from the file. A request with a matching If-None-Match gets a 304 with no
body, repeating ETag and Cache-Control; one that does not match gets the
full file again.
That is what makes the browser download the widgets once per space instead of once per function, even while the index keeps swapping the iframe.