diff --git a/backend/app/main.py b/backend/app/main.py index d3fb78f..721712c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -112,6 +112,20 @@ async def setup_gate(request, call_next): return await call_next(request) +@app.middleware("http") +async def static_cache_headers(request, call_next): + """Long-cache the content-hashed SPA bundles. Vite hashes their filename, so a given + /assets URL never changes content and a browser can hold it forever — this is what makes + repeat loads instant and clears the Lighthouse "efficient cache lifetimes" audit. index.html + stays no-cache (set on its own FileResponse) so a deploy is picked up at once; other + SPA-root static files (welcome images, favicon, robots.txt) get a moderate TTL in the SPA + fallback below.""" + response = await call_next(request) + if request.url.path.startswith("/assets/"): + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + return response + + app.include_router(health.router) app.include_router(auth.router) app.include_router(sync.router) @@ -134,6 +148,13 @@ app.include_router(quota.router) app.include_router(version.router) app.include_router(setup_routes.router) +# Ensure modern image types resolve to the right Content-Type when FileResponse guesses from the +# filename (the runtime's mimetypes db doesn't always know .webp → it'd fall back to octet-stream). +import mimetypes + +mimetypes.add_type("image/webp", ".webp") +mimetypes.add_type("image/avif", ".avif") + # The built SPA (populated by the Docker frontend build stage). STATIC_DIR = Path(__file__).parent / "static_spa" # index.html is unhashed and references the content-hashed /assets bundles, so it MUST NOT be @@ -166,5 +187,7 @@ async def spa_fallback(full_path: str) -> FileResponse: if full_path: candidate = (STATIC_DIR / full_path).resolve() if candidate.is_file() and STATIC_DIR.resolve() in candidate.parents: - return FileResponse(candidate) + # Real SPA-root assets (welcome images, favicon, robots.txt) rarely change and have + # stable names, so a moderate cache is safe and satisfies the cache-lifetime audit. + return FileResponse(candidate, headers={"Cache-Control": "public, max-age=604800"}) return FileResponse(INDEX_HTML, headers=INDEX_HEADERS) diff --git a/frontend/public/welcome/channels.png b/frontend/public/welcome/channels.png deleted file mode 100644 index 20391e4..0000000 Binary files a/frontend/public/welcome/channels.png and /dev/null differ diff --git a/frontend/public/welcome/channels.webp b/frontend/public/welcome/channels.webp new file mode 100644 index 0000000..404a67d Binary files /dev/null and b/frontend/public/welcome/channels.webp differ diff --git a/frontend/public/welcome/feed.png b/frontend/public/welcome/feed.png deleted file mode 100644 index 26de65d..0000000 Binary files a/frontend/public/welcome/feed.png and /dev/null differ diff --git a/frontend/public/welcome/feed.webp b/frontend/public/welcome/feed.webp new file mode 100644 index 0000000..33db8a0 Binary files /dev/null and b/frontend/public/welcome/feed.webp differ diff --git a/frontend/public/welcome/playlists.png b/frontend/public/welcome/playlists.png deleted file mode 100644 index 6792c4d..0000000 Binary files a/frontend/public/welcome/playlists.png and /dev/null differ diff --git a/frontend/public/welcome/playlists.webp b/frontend/public/welcome/playlists.webp new file mode 100644 index 0000000..9512288 Binary files /dev/null and b/frontend/public/welcome/playlists.webp differ diff --git a/frontend/src/components/Welcome.tsx b/frontend/src/components/Welcome.tsx index f21de8e..56b3ce1 100644 --- a/frontend/src/components/Welcome.tsx +++ b/frontend/src/components/Welcome.tsx @@ -59,7 +59,7 @@ export default function Welcome() { {/* App preview */}