Docs
Static assets
Serve static files and embedded assets safely.
FrameworkBuilder nests your static router at /static, and you supply that router, so the service, cache headers, and deployment rules stay yours. This page covers disk-backed assets, embedded assets, linked island assets, the CSS manifest, and external apps.
Serving assets from disk
Use tower_http::services::ServeDir for files built into a directory:
use axum::Router;
use tower::ServiceBuilder;
use tower_http::services::ServeDir;
use tower_http::set_header::SetResponseHeaderLayer;
use axum::http::header::{HeaderValue, CACHE_CONTROL, X_CONTENT_TYPE_OPTIONS};
let static_router = Router::new().nest_service(
"/assets",
ServiceBuilder::new()
.layer(SetResponseHeaderLayer::if_not_present(
CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),
))
.layer(SetResponseHeaderLayer::if_not_present(
X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
))
.service(ServeDir::new("frontend/dist/assets")),
);
let app = FrameworkBuilder::new()
.with_static_service(static_router)
.build();
The final URL for the nested example is /static/assets/.... When deployed
under a public prefix, configure FrameworkBuilder::with_base_path and use
cx.url("/static/assets/...") in templates. The manifest CSS link is
prefixed automatically. See URL prefix deployments.
When with_base_path(...) is configured, the managed service also composes
recognized URLs inside CSS and JS/MJS files. CSS url(...)/image-set(...)/@import, module
imports, and native browser request/worker URL arguments follow the mount.
Ordinary strings and external URLs stay unchanged. Dynamic browser code can
coverage and examples.
For routers built without FrameworkBuilder, wrap the static router explicitly:
use proa::axum::{base_path_assets, BasePath};
let static_router = base_path_assets(static_router, BasePath::new("/apps/demo")?);
Apply compression outside this wrapper. Its parsed output is cached by content; ETags and HEAD responses describe the transformed representation. Integrity hashes must be computed from those served bytes.
A single-binary deploy skips the directory entirely and carries the files inside the executable.
Embedding assets in the binary
Use EmbeddedStaticService when include_dir compiles assets into the binary.
use axum::Router;
use include_dir::{include_dir, Dir};
use proa::axum::EmbeddedStaticService;
static PUBLIC: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/public");
let static_router = Router::new().nest_service(
"/",
EmbeddedStaticService::new(PUBLIC.clone()),
);
Dir holds only 'static slices, so the clone copies references, not file bytes.
The embedded service sets a content type from the extension and adds X-Content-Type-Options: nosniff.
Either service can host the stylesheet, but the page still needs a link to it.
Serving linked island assets
RSJS embeds its runtimes and generated module sources in the linked binary.
Enable their HTTP routes with with_rsjs_assets():
use proa::axum::FrameworkBuilder;
let app = FrameworkBuilder::new()
.with_rsjs_assets()
.with_dynamic_routes_and_metadata(routes, metadata)
.build();
| Route | Serves |
|---|---|
/rsjs-runtime.js | Core browser runtime. |
/rsjs-runtime-query.js | Query and mutation runtime. |
/rsjs/island/:file | Linked island modules, client helpers/classes, and component fragments. |
The response finalizer selects the script tags and preloads each page needs;
these routes serve the corresponding sources. with_static_service(...)
continues to serve application files under /static independently.
Use these routes in place of custom handlers at the same paths to avoid route
collisions. For a custom Axum router, merge
proa::axum::rsjs_assets_router(&base_path) and apply
RouteContextLayer::with_base_path with the same setting.
with_rsjs_assets() works with the default root or with
with_base_path(...).
With a prefix, it rewrites runtime import URLs once at startup, while incoming
routes remain unprefixed and HTML responses keep streaming. See
URL prefix deployments
for proxy setup and RSJS runtime wiring for asset selection
and hydration.
Linking the application stylesheet
Write the link in the document layout. cx.url applies the configured base path:
let stylesheet = cx.url("/static/assets/app.css");
html_sync! {
<link rel="stylesheet" href={stylesheet} />
}
Mounting external apps
Compose additional static applications with Axum's router APIs. For example,
nest a file service at /docs. Configure any SPA fallback explicitly on that
service. Proa does not start other dev servers or provide a reverse proxy.
Every path above serves files straight to the browser, which makes what you put in those directories a security decision.
Securing served directories
- Serve only trusted build output from static directories.
- Do not let untrusted users write files or symlinks into served asset directories.
- Add
nosniffheaders for disk-backed services. - Prefer immutable cache headers only for fingerprinted asset names.
- Keep uploads behind explicit authenticated handlers instead of a generic static file service.
Next steps
- Page caching
- How static asset cache headers fit with HTML and endpoint caching.
- Images
- Serve responsive images from static assets, without a build pipeline.
- Fonts
- Self-host web fonts with metric-matched fallbacks and zero runtime requests.
- Configuration
- Configure FrameworkBuilder, security defaults, static mounts, and response behavior.