feat: PWA update strategy with hashed assets and cache control

Move frontend sources to frontend/, build content-hashed assets at deploy,
network-first SW for shell/JS, immutable cache for /assets/, version.json
update banner, and Cache-Control middleware. Docs in docs/PWA-STRATEGY.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-07-08 08:51:12 +02:00
parent bff14167c9
commit 7cec3030c1
29 changed files with 779 additions and 61 deletions
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
from starlette.types import ASGIApp, Receive, Scope, Send
NO_CACHE = b"no-cache, must-revalidate"
NO_STORE = b"no-store"
IMMUTABLE = b"public, max-age=31536000, immutable"
def _cache_control_for_path(path: str) -> bytes | None:
if path in ("/sw.js", "/index.html", "/manifest.json"):
return NO_CACHE
if path == "/version.json":
return NO_STORE
if path.startswith("/assets/"):
return IMMUTABLE
if path.endswith((".js", ".css")) and not path.startswith("/assets/"):
return NO_CACHE
return None
class CacheControlMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
path = scope.get("path", "")
cache_control = _cache_control_for_path(path)
async def send_wrapper(message: dict) -> None:
if message["type"] == "http.response.start" and cache_control is not None:
headers = list(message.get("headers", []))
headers = [(k, v) for k, v in headers if k.lower() != b"cache-control"]
headers.append((b"cache-control", cache_control))
message = {**message, "headers": headers}
await send(message)
await self.app(scope, receive, send_wrapper)