From a0305282efd4e850c75e72a52ef39676e53046a8 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 24 Aug 2026 22:40:29 +0200 Subject: [PATCH] Revalidating cache on app assets; site title survives SPA navigation pkg files keep stable names across releases, so an uncontrolled browser cache could pair last release's wasm with the new server's server-fn wire format - every page then renders its error branch (redoal.com's 'Nothing here' after v0.2.0). Cache-Control: no-cache on /pkg and /yes.js makes clients revalidate (a 304 per load) instead of guessing. The content-declared site title also moves out of the question Suspense: remounting with each navigation lost the leptos_meta race to the compile-time fallback, flipping redoal.com's tab to 'uhhm' on the first client-side nav. Co-Authored-By: Claude Fable 5 --- Cargo.toml | 2 +- src/app.rs | 19 ++++++++++++------- src/main.rs | 29 +++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3603e64..3d7ac96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ axum = { version = "0.8", features = ["multipart"], optional = true } aws-sdk-s3 = { version = "1", optional = true } tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], optional = true } tower = { version = "0.5", optional = true } -tower-http = { version = "0.6", features = ["fs"], optional = true } +tower-http = { version = "0.6", features = ["fs", "set-header"], optional = true } tower-sessions = { version = "0.14", optional = true } async-nats = { version = "0.38", optional = true } arc-swap = { version = "1", optional = true } diff --git a/src/app.rs b/src/app.rs index 1fc9633..5daa6f3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -89,6 +89,18 @@ fn QuestionPage() -> impl IntoView { let site = Resource::new(|| (), |_| get_site()); view! { + // The content-declared site title lives OUTSIDE the question + // Suspense: tied to the per-page resource it unmounted on every + // SPA navigation and lost the leptos_meta race to App's + // compile-time SITE_NAME fallback (redoal.com flashing to + // "uhhm" on nav - and staying there). Out here it mounts once + // and covers every branch, NotFound included. + {move || { + site.get() + .and_then(|r| r.ok()) + .and_then(|s| s.title) + .map(|t| view! { }) + }} <Suspense fallback=|| { view! { <main class="loading"> @@ -109,13 +121,6 @@ fn QuestionPage() -> impl IntoView { Ok(Some(page)) => { let current = user_res.and_then(|r| r.ok()).flatten(); view! { - // A second <Title> outranks App's - // SITE_NAME fallback only when content - // actually declares one. - {site_cfg - .title - .clone() - .map(|t| view! { <Title text=t/> })} <QuestionView question=page.question chain_gate=page.chain_gate diff --git a/src/main.rs b/src/main.rs index 166ce82..786a876 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,7 +143,22 @@ async fn main() -> anyhow::Result<()> { .route("/upload", post(upload::upload)) .route("/gitea-repo", get(content::gitea_repo_handler)) .route("/automation/kv/{bucket}", get(content::automation_kv_handler)) - .nest_service("/pkg", ServeDir::new(pkg_dir)) + // no-cache = "revalidate before reuse", not "don't cache": + // pkg files keep the same names across releases (portal.js, + // portal.wasm), and without this browsers heuristically cache + // them - a stale wasm from the previous release then talks to + // a server whose server-fn wire format has moved on and every + // page renders as its error branch. A 304 per load is the + // price of never shipping that skew again. + .nest_service( + "/pkg", + tower::ServiceBuilder::new() + .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( + axum::http::header::CACHE_CONTROL, + axum::http::HeaderValue::from_static("no-cache"), + )) + .service(ServeDir::new(pkg_dir)), + ) .nest_service("/fonts", ServeDir::new(fonts_dir)) .route_service("/favicon-light.svg", ServeFile::new(favicon_light_path)) .route_service("/favicon-dark.svg", ServeFile::new(favicon_dark_path)) @@ -152,7 +167,17 @@ async fn main() -> anyhow::Result<()> { "/swiper-element-bundle.min.js", ServeFile::new(swiper_path), ) - .route_service("/yes.js", ServeFile::new(yes_path)) + // Same skew concern as /pkg: the wasm's raw_module import and + // the hero's inline script both load this by fixed name. + .route_service( + "/yes.js", + tower::ServiceBuilder::new() + .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( + axum::http::header::CACHE_CONTROL, + axum::http::HeaderValue::from_static("no-cache"), + )) + .service(ServeFile::new(yes_path)), + ) .leptos_routes_with_context( &state, routes,