All checks were successful
Adds a `static_v(request, name, path=...)` Jinja helper that appends
?v=<commit-sha> from app.core.build_info, plus a CachedStaticFiles
subclass that serves Cache-Control: public, max-age=31536000, immutable
in production and no-cache in development. Browsers refetch JS/CSS
automatically on every deploy without the user having to hard-reload.
- New: app/core/static_files.py (CachedStaticFiles)
- Updated: app/templates_config.py (static_v helper)
- Updated: main.py (use CachedStaticFiles for *_static mounts)
- Codemod: 143 url_for('*_static', path='*.js'|'*.css') → static_v(...)
across 123 templates. Images/fonts/JSON locales intentionally
unchanged (out of scope).
- Arch rule: FE-024 (warning) flags raw url_for on JS/CSS to prevent
drift. Note: FE-008 was already taken by the number_stepper rule.
- docs/proposals/static-asset-cache-busting.md marked Done.
Closes plan from docs/proposals/static-asset-cache-busting.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 lines
1.1 KiB
Python
32 lines
1.1 KiB
Python
# app/core/static_files.py
|
|
"""Static file serving with cache-busting-aware headers.
|
|
|
|
In production, JS/CSS/etc. URLs are versioned by `static_v(...)` in
|
|
`app/templates_config.py` (appends `?v=<commit-sha>`), so the file at a
|
|
given URL never changes within a deploy. We can therefore tell browsers
|
|
to cache it for a year and skip revalidation.
|
|
|
|
In development the version still flips per commit, but the developer often
|
|
edits files without committing, so we serve `no-cache` to force conditional
|
|
GETs (the browser still gets a 304 when nothing changed).
|
|
"""
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
from starlette.responses import FileResponse
|
|
|
|
from app.core.environment import is_development
|
|
|
|
_IMMUTABLE = "public, max-age=31536000, immutable"
|
|
_NO_CACHE = "no-cache"
|
|
|
|
|
|
class CachedStaticFiles(StaticFiles):
|
|
"""StaticFiles that sets Cache-Control based on environment."""
|
|
|
|
def file_response(self, *args, **kwargs) -> FileResponse:
|
|
response = super().file_response(*args, **kwargs)
|
|
response.headers["Cache-Control"] = (
|
|
_NO_CACHE if is_development() else _IMMUTABLE
|
|
)
|
|
return response
|