Docs

Static assets

Serve static files and embedded assets safely.

Open Markdown

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:

src/app.rs
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.

src/app.rs
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():

src/app.rs
use proa::axum::FrameworkBuilder;

let app = FrameworkBuilder::new()
    .with_rsjs_assets()
    .with_dynamic_routes_and_metadata(routes, metadata)
    .build();
RouteServes
/rsjs-runtime.jsCore browser runtime.
/rsjs-runtime-query.jsQuery and mutation runtime.
/rsjs/island/:fileLinked 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:

src/layouts/root.rs
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

Next steps

Search

Type at least 2 characters