feat: cache-control overhaul so visual changes propagate immediately
Per-file content-hash versioning on every /static reference, immutable cache headers on versioned URLs, no-cache on HTML, auto-bumped service worker cache name with stale-while-revalidate for assets, and a controllerchange listener that silently reloads the page when a new SW takes control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+71
-1
@@ -1,5 +1,10 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from flask import Flask
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, request
|
||||
|
||||
from app.config import LOG_LEVEL
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -10,4 +15,69 @@ logging.basicConfig(
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
# ── Static asset versioning ────────────────────────────────────────
|
||||
# Each /static/<file> reference gets a ?v=<hash> query string so we can serve
|
||||
# it with `Cache-Control: immutable` and still bust the cache when bytes change.
|
||||
_FALLBACK_TOKEN = hashlib.sha1(str(time.time()).encode()).hexdigest()[:8]
|
||||
_static_hashes: dict[str, str] = {}
|
||||
|
||||
|
||||
def _hash_file(path: Path) -> str:
|
||||
h = hashlib.sha1()
|
||||
with path.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()[:8]
|
||||
|
||||
|
||||
def static_hash(filename: str) -> str:
|
||||
if filename in _static_hashes:
|
||||
return _static_hashes[filename]
|
||||
path = Path(app.static_folder) / filename
|
||||
try:
|
||||
token = _hash_file(path)
|
||||
except OSError:
|
||||
logging.getLogger(__name__).warning(
|
||||
"static_hash: cannot hash %s, using fallback token", filename
|
||||
)
|
||||
return _FALLBACK_TOKEN
|
||||
_static_hashes[filename] = token
|
||||
return token
|
||||
|
||||
|
||||
def static_v(filename: str) -> str:
|
||||
return f"/static/{filename}?v={static_hash(filename)}"
|
||||
|
||||
|
||||
_static_dir = Path(app.static_folder)
|
||||
if _static_dir.is_dir():
|
||||
for _path in sorted(_static_dir.iterdir()):
|
||||
if _path.is_file():
|
||||
try:
|
||||
_static_hashes[_path.name] = _hash_file(_path)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
APP_VERSION = (
|
||||
hashlib.sha1(
|
||||
"|".join(f"{n}:{h}" for n, h in sorted(_static_hashes.items())).encode()
|
||||
).hexdigest()[:8]
|
||||
if _static_hashes
|
||||
else _FALLBACK_TOKEN
|
||||
)
|
||||
|
||||
app.jinja_env.globals["static_v"] = static_v
|
||||
|
||||
|
||||
@app.after_request
|
||||
def _add_cache_headers(response):
|
||||
if response.mimetype == "text/html":
|
||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
return response
|
||||
if request.path.startswith("/static/") and request.args.get("v"):
|
||||
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||
return response
|
||||
|
||||
|
||||
from app import routes # noqa: E402, F401
|
||||
|
||||
Reference in New Issue
Block a user