//! Serving the built web UI out of the binary. //! //! `rust-embed` compiles `web/dist` in for release builds, so a deployment is //! still one file. In debug builds it reads from disk instead, so `vite build` //! output is picked up without a `cargo` rebuild. //! //! Anything that is not an embedded asset falls back to `index.html`, because the //! UI routes client-side and a deep link must not 404. use axum::http::{StatusCode, Uri, header}; use axum::response::{IntoResponse, Response}; #[derive(rust_embed::Embed)] #[folder = "../../web/dist"] struct Assets; /// The message shown when the binary was built without a web UI. Silence here /// would look like a broken deployment rather than a missing build step. const NO_BUILD: &str = "the web UI is not built. Run `cd web && bun install && bun run build`."; pub async fn serve(uri: Uri) -> Response { let path = uri.path().trim_start_matches('/'); if let Some(response) = asset(path) { return response; } // Never fall back for asset-shaped requests: a missing chunk answered with // HTML turns a clear 404 into a confusing MIME-type error in the console. if path.starts_with("assets/") { return (StatusCode::NOT_FOUND, "not found").into_response(); } asset("index.html").unwrap_or_else(|| (StatusCode::NOT_FOUND, NO_BUILD).into_response()) } fn asset(path: &str) -> Option { let file = Assets::get(path)?; let mime = mime_guess::from_path(path).first_or_octet_stream(); // Vite content-hashes everything under assets/, so those are immutable. // index.html must not be, or a deploy would never reach an open tab. let cache = if path.starts_with("assets/") { "public, max-age=31536000, immutable" } else { "no-cache" }; Some( ( [ (header::CONTENT_TYPE, mime.as_ref()), (header::CACHE_CONTROL, cache), ], file.data.into_owned(), ) .into_response(), ) }