fix(deploy): bust the app-shell cache — no-cache _headers + content-versioned asset urls
Pages caches .js/.css by type (max-age=14400) and ignores _headers for them, so returning visitors ran stale JS (no-video/no-audio until a private window). index.html is no-cache and now references app.js?v=<hash> etc., so a normal reload always gets fresh code; media on R2 stays immutable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ sibling tools/http.py shadows the stdlib `http` that fastapi/starlette import:
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
@@ -67,6 +68,38 @@ def _write_redirects(out: Path, seg: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _write_headers(out: Path, seg: str) -> None:
|
||||
# The app shell (html/js/json) must NOT be browser-cached — otherwise a deploy is
|
||||
# invisible to returning visitors for hours (Pages' default is max-age=14400).
|
||||
# `no-cache` = store but revalidate every load (cheap 304 via etag). The media is
|
||||
# on R2 (immutable + content-hash), unaffected. Cloudflare Pages `_headers` syntax.
|
||||
(out / "_headers").write_text(
|
||||
f"/{seg}/*\n"
|
||||
" Cache-Control: no-cache\n"
|
||||
)
|
||||
|
||||
|
||||
def _version_assets(app_dir: Path) -> None:
|
||||
# Content-version the app-shell asset URLs in the HTML so a new build = a new URL
|
||||
# = guaranteed-fresh fetch on a normal reload. The HTML itself is served no-cache
|
||||
# (_headers), but Cloudflare Pages caches .js/.css by type (max-age=14400) and
|
||||
# ignores _headers for them — so without this, returning visitors run stale JS.
|
||||
assets = ["app.js", "scrub.js", "i18n.js", "alteration.js", "style.css", "credits.js", "config.js"]
|
||||
tok = {}
|
||||
for a in assets:
|
||||
p = app_dir / a
|
||||
if p.exists():
|
||||
tok[a] = hashlib.sha1(p.read_bytes()).hexdigest()[:12]
|
||||
for html in ("index.html", "credits.html"):
|
||||
hp = app_dir / html
|
||||
if not hp.exists():
|
||||
continue
|
||||
s = hp.read_text()
|
||||
for a, h in tok.items():
|
||||
s = s.replace(f'src="{a}"', f'src="{a}?v={h}"').replace(f'href="{a}"', f'href="{a}?v={h}"')
|
||||
hp.write_text(s)
|
||||
|
||||
|
||||
def build_static(out_dir, media_out, *, media_base: str, app_path: str) -> dict:
|
||||
out = Path(out_dir)
|
||||
media = Path(media_out)
|
||||
@@ -85,7 +118,9 @@ def build_static(out_dir, media_out, *, media_base: str, app_path: str) -> dict:
|
||||
|
||||
versions = _bake_api(app_dir)
|
||||
_write_config(app_dir, media_base)
|
||||
_version_assets(app_dir) # after config.js exists, so it gets hashed too
|
||||
_write_redirects(out, seg)
|
||||
_write_headers(out, seg)
|
||||
|
||||
# Sync ONLY referenced media: `versions` covers clip bases + transition morphs;
|
||||
# add each scale's audio explicitly. Never the master/mezzanine pipeline sources.
|
||||
|
||||
Reference in New Issue
Block a user