2025-03-20 09:22:31 +01:00
|
|
|
use crate::common::config::AppConfig;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::common::di::AppState;
|
2026-06-17 17:06:30 -06:00
|
|
|
use axum::Router;
|
2026-02-17 19:29:42 +01:00
|
|
|
use axum::http::header::{CACHE_CONTROL, HeaderValue};
|
2026-06-17 17:06:30 -06:00
|
|
|
use axum::routing::get_service;
|
|
|
|
|
use std::path::Path;
|
2026-02-24 15:11:56 +01:00
|
|
|
use std::sync::Arc;
|
2026-02-17 19:29:42 +01:00
|
|
|
use tower_http::compression::CompressionLayer;
|
2026-06-17 17:06:30 -06:00
|
|
|
use tower_http::services::{ServeDir, ServeFile};
|
2026-02-17 19:29:42 +01:00
|
|
|
use tower_http::set_header::SetResponseHeaderLayer;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2026-06-17 17:06:30 -06:00
|
|
|
/// Serves the SvelteKit single-page app.
|
|
|
|
|
///
|
|
|
|
|
/// The frontend is built by Vite into `static-dist/` (repo root). Real files are
|
|
|
|
|
/// served from disk; any unmatched client route (deep links such as
|
|
|
|
|
/// `/files/<id>`, `/s/<token>`, `/login`) falls back to the SPA shell
|
|
|
|
|
/// `index.html`, which boots the client router.
|
|
|
|
|
///
|
|
|
|
|
/// Caching: content-hashed assets under `/_app/immutable` are cached forever;
|
|
|
|
|
/// everything else — crucially the `index.html` shell — is `no-cache` so a deploy
|
|
|
|
|
/// can't leave a stale app pinned in browsers.
|
2026-02-24 15:11:56 +01:00
|
|
|
pub fn create_web_routes() -> Router<Arc<AppState>> {
|
2025-03-20 09:22:31 +01:00
|
|
|
let config = AppConfig::from_env();
|
2026-03-08 13:10:38 +01:00
|
|
|
|
2026-06-17 17:06:30 -06:00
|
|
|
// `PROFILE=dev` (the `just front-dev`/legacy path) serves the unbuilt source
|
|
|
|
|
// dir; normal release serves the Vite output in `static-dist/`.
|
2026-03-22 22:30:29 +01:00
|
|
|
let is_dev = std::env::var("PROFILE").is_ok_and(|profile| profile == "dev");
|
2026-06-17 17:06:30 -06:00
|
|
|
let assets_dir = if is_dev { "static" } else { "static-dist" };
|
2026-03-22 22:30:29 +01:00
|
|
|
|
2026-03-08 13:10:38 +01:00
|
|
|
let static_path = if cfg!(not(debug_assertions)) {
|
|
|
|
|
let dist = config
|
|
|
|
|
.static_path
|
|
|
|
|
.parent()
|
2026-06-17 17:06:30 -06:00
|
|
|
.unwrap_or(Path::new("."))
|
2026-03-26 09:46:50 +01:00
|
|
|
.join(assets_dir);
|
2026-03-09 14:34:07 +01:00
|
|
|
if dist.exists() {
|
|
|
|
|
dist
|
|
|
|
|
} else {
|
|
|
|
|
config.static_path.clone()
|
|
|
|
|
}
|
2026-03-08 13:10:38 +01:00
|
|
|
} else {
|
|
|
|
|
config.static_path.clone()
|
|
|
|
|
};
|
2025-03-20 09:22:31 +01:00
|
|
|
|
2026-06-17 17:06:30 -06:00
|
|
|
// SPA fallback: serve the file if it exists, else the app shell.
|
|
|
|
|
let spa = ServeDir::new(&static_path).fallback(ServeFile::new(static_path.join("index.html")));
|
|
|
|
|
|
|
|
|
|
// Hashed, immutable assets (SvelteKit emits these under /_app/immutable).
|
|
|
|
|
let app_immutable = ServeDir::new(static_path.join("_app").join("immutable"));
|
2026-02-17 19:29:42 +01:00
|
|
|
|
2026-06-17 17:06:30 -06:00
|
|
|
let shell_cache = if is_dev {
|
2026-03-26 09:46:50 +01:00
|
|
|
"max-age=0, no-cache, no-store"
|
|
|
|
|
} else {
|
2026-06-17 17:06:30 -06:00
|
|
|
"no-cache"
|
2026-03-26 09:46:50 +01:00
|
|
|
};
|
2026-03-22 22:30:29 +01:00
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
Router::new()
|
2026-06-17 17:06:30 -06:00
|
|
|
.nest_service(
|
|
|
|
|
"/_app/immutable",
|
|
|
|
|
get_service(app_immutable).layer(SetResponseHeaderLayer::overriding(
|
|
|
|
|
CACHE_CONTROL,
|
|
|
|
|
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.fallback_service(spa)
|
2026-02-17 22:58:56 -08:00
|
|
|
.layer(CompressionLayer::new().br(true).gzip(true))
|
2026-06-17 17:06:30 -06:00
|
|
|
// `if_not_present` so the immutable assets above keep their long cache.
|
2026-02-17 19:29:42 +01:00
|
|
|
.layer(SetResponseHeaderLayer::if_not_present(
|
|
|
|
|
CACHE_CONTROL,
|
2026-06-17 17:06:30 -06:00
|
|
|
HeaderValue::from_static(shell_cache),
|
2026-02-17 19:29:42 +01:00
|
|
|
))
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|