From d8431d8e0e04490a6c5b9eda5fa9a239c56310eb Mon Sep 17 00:00:00 2001 From: JingWen Fan <106414602+study8677@users.noreply.github.com> Date: Fri, 22 May 2026 11:57:03 +0800 Subject: [PATCH] fix: set SPA cache headers to prevent stale-shell blank page after deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SPA shell (index.html) was served with no Cache-Control header, so browsers cached it heuristically. After a deploy renames the content-hashed JS/CSS chunks (and rsync --delete removes the old ones), a cached old shell points at now-404 assets and React never boots — a blank/white page. Serve the shell and swappable static files (logo, contact-qr) with `no-cache` (always revalidate), and the content-hashed `assets/` files with `immutable, max-age=1y` so they still cache long-term. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/opencmo/web/app.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/opencmo/web/app.py b/src/opencmo/web/app.py index a9f1d80..fafea19 100644 --- a/src/opencmo/web/app.py +++ b/src/opencmo/web/app.py @@ -1865,7 +1865,19 @@ async def spa_catchall(request: Request, full_path: str = ""): if spa_root in asset.parents and asset.exists() and asset.is_file(): import mimetypes ct = mimetypes.guess_type(str(asset))[0] or "application/octet-stream" - return StreamingResponse(open(asset, "rb"), media_type=ct) + # Vite emits content-hashed files under assets/ — cache them + # forever. Other files (logo, contact-qr, etc.) can be swapped in + # place, so keep them revalidated. + cache_control = ( + "public, max-age=31536000, immutable" + if full_path.startswith("assets/") + else "no-cache" + ) + return StreamingResponse( + open(asset, "rb"), + media_type=ct, + headers={"Cache-Control": cache_control}, + ) new_visitor_id: str | None = None try: @@ -1880,6 +1892,9 @@ async def spa_catchall(request: Request, full_path: str = ""): # SPA fallback — always return index.html response = HTMLResponse(rendered_html) + # The shell references content-hashed assets that change every build, so it + # must be revalidated to avoid serving a stale shell (blank page) post-deploy. + response.headers["Cache-Control"] = "no-cache" if _is_app_surface(full_path): response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive, nosnippet" if new_visitor_id: