---
title: Static assets
description: 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](#serving-assets-from-disk), [embedded assets](#embedding-assets-in-the-binary), [linked island assets](#serving-linked-island-assets), [the CSS manifest](#linking-the-css-manifest), and [external apps](#mounting-external-apps).

## Serving assets from disk

Use `tower_http::services::ServeDir` for files built into a directory:

```rust filename="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](/docs/framework/deployment#serving-behind-a-url-path-prefix).

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](/docs/framework/deployment#coverage-and-limitations).

For routers built without `FrameworkBuilder`, wrap the static router explicitly:

```rust
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.

```rust filename="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()`:

```rust filename="src/app.rs"
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(...)`](/docs/framework/configuration#frameworkbuilder-base-paths-and-rsjs-assets).
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](/docs/framework/deployment#serving-behind-a-url-path-prefix)
for proxy setup and [RSJS runtime wiring](/docs/rsjs/runtime) for asset selection
and hydration.

## Linking the application stylesheet

Write the link in the document layout. `cx.url` applies the configured base path:

```rust filename="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

- Serve **only** trusted build output from static directories.
- Do not let untrusted users write files or symlinks into served asset directories.
- Add `nosniff` headers 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](/docs/framework/page-caching)
  - How static asset cache headers fit with HTML and endpoint caching.
- [Images](/docs/framework/images)
  - Serve responsive images from static assets, without a build pipeline.
- [Fonts](/docs/framework/fonts)
  - Self-host web fonts with metric-matched fallbacks and zero runtime requests.
- [Configuration](/docs/framework/configuration)
  - Configure FrameworkBuilder, security defaults, static mounts, and response behavior.
