# tower-serve-embedded
Embed content-hashed, cache-busted static web assets (CSS, JS, images) into your Rust binary —
and serve them from any [`tower`](https://crates.io/crates/tower)/[`axum`](https://crates.io/crates/axum)
stack.
It's in the same family as [`rust-embed`](https://crates.io/crates/rust-embed), but tuned for
*serving* web assets:
- **Content hashing + cache busting.** Every file is exposed at a hashed URL that mirrors its
location in your project — `assets/css/style.css` → `/assets/css/style.9f3a1c2b.css` — so it
can be served `immutable` with a one-year cache and still update the instant its content changes.
- **A compile-time `asset!` macro.** `asset!("assets/css/style.css")` expands to the hashed URL as
a `&'static str` for SSR templates (e.g. [`maud`](https://maud.lambda.xyz)). Typos are compile
errors.
- **A `tower::Service`** that serves the embedded assets with `Content-Type`, `ETag`/`304`, and
`Cache-Control`. Drop it into axum or any other `tower`-based stack.
Design goals: **simple** (point it at a directory, done), **hot-reloadable** in development (via
`build.rs` + `cargo:rerun-if-changed`, so `cargo watch` rebuilds on change), and **no proc
macros** — all codegen happens in a build script, so IDE support stays sharp.
There's no URL-prefix to configure: an asset's path under your crate root *is* its URL.
## Crates
| `tower-serve-embedded` | `[dependencies]` | Runtime types, the `asset!` macro, the tower service. |
| `tower-serve-embedded-build` | `[build-dependencies]` | Walks the asset dir, hashes files, generates code. |
## Quickstart
```toml
# Cargo.toml
[dependencies]
tower-serve-embedded = "0.1"
[build-dependencies]
tower-serve-embedded-build = "0.1"
```
```rust
// build.rs
fn main() {
tower_serve_embedded_build::Builder::new("assets") // dir relative to your crate root
// .ignore_dir("lib") // skip a subdir, e.g. vendored deps
.emit()
.unwrap();
}
```
```rust
// src/main.rs (or any module)
tower_serve_embedded::embed!(); // brings `ASSETS` and the `asset!` macro into scope
```
### Reference assets in templates
Refer to assets by their path **relative to the crate root**. `asset!` resolves at compile time
to the full, cache-busted URL:
```rust
let css = asset!("assets/css/style.css"); // "/assets/css/style.9f3a1c2b.css"
```
It drops straight into any engine that interpolates `&str`, e.g. `maud`:
```rust
html! {
link rel="stylesheet" href=(asset!("assets/css/style.css"));
script src=(asset!("assets/js/app.js")) defer {}
}
```
Need a name that isn't known at compile time? Use the runtime equivalent:
```rust
let url: Option<&'static str> = ASSETS.url("assets/css/style.css");
```
### Serve them
Each embedded path is already the full URL, so mount the service as a **fallback** — no nesting
or prefix to keep in sync:
```rust
use axum::{Router, routing::get};
let app = Router::new()
.route("/", get(index))
.fallback_service(ASSETS.service());
```
The service handles `GET`/`HEAD`, returns `304 Not Modified` for matching `If-None-Match`, and
sets `Content-Type`, a strong `ETag`, and `Cache-Control: public, max-age=31536000, immutable`.
It's a plain `tower::Service`, so wrap it in any `tower` layer (compression, tracing, …) you like.
> Prefer not to use the fallback slot? Scope it to the asset directory instead — the service
> still matches the full path, so route it without stripping the prefix:
> `Router::route_service("/assets/{*path}", ASSETS.service())`.
## Hot reloading
The build script emits `cargo:rerun-if-changed` for the asset directory and every file, so:
```sh
cargo watch -x run
```
Editing an asset re-runs the build script, recomputes the hash, and rebuilds — the new
cache-busted URL flows through `asset!` automatically. Dev and release behave identically; there's
no separate "debug" embedding mode to reason about.
Two mechanisms cooperate here, and it's worth knowing they're distinct:
- `cargo watch` is a filesystem watcher; its job is to **invoke cargo** when a file changes. With
no `-w` flags it watches the whole crate (both `src` and `assets`, ignoring `target`), so you
don't need to enumerate paths.
- The build script's `cargo:rerun-if-changed` lines are a **cargo** signal that decides whether to
re-run the build script *once cargo is invoked*. They don't watch the filesystem, so they don't
replace `cargo watch` — they're what makes the rebuild actually pick up the new bytes and hash.
## Notes
- Hashing is **BLAKE3**, truncated to 16 hex chars (64 bits) for filenames and ETags.
- Hidden files/directories (names starting with `.`) and symlinks are skipped.
- One asset set per crate: the generated `asset!` is `#[macro_export]`ed at your crate root.
- Every served asset is treated as immutable. A stable, always-fresh HTML entry point (SPA
`index.html`) is intentionally out of scope — keep that route in your app.
## Examples
Runnable, self-contained setups live in [`examples/`](examples), each with its own `build.rs`,
`assets/`, and `main.rs`:
| [`axum`](examples/axum) | `cargo run -p axum-example` | Mounts `ASSETS.service()` (a `tower::Service`) as a fallback. |
| [`actix`](examples/actix) | `cargo run -p actix-example` | Uses the framework-agnostic data API (`ASSETS.get`). |
| [`warp`](examples/warp) | `cargo run -p warp-example` | Uses the framework-agnostic data API (`ASSETS.get`). |
axum is `tower`-based, so it mounts the service directly. actix-web and warp aren't, so they look
assets up with [`Assets::get`] and build the response by hand — the `asset!` macro and the
`build.rs` embedding step are identical across all three.
[`Assets::get`]: https://docs.rs/tower-serve-embedded/latest/tower_serve_embedded/struct.Assets.html#method.get
## License
MIT OR Apache-2.0.