From e680fda8bd9d76640737dc7f0891ef544c4f2280 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sun, 17 May 2026 22:35:24 +0200 Subject: [PATCH] docs(proposals): static asset cache-busting plan Proposes a one-liner Jinja helper that appends ?v= to static JS/CSS URLs, leveraging the existing build_info pipeline so users no longer need to hard-reload after every deploy. Documents the codemod scope (143 callsites), open decisions, and the server-side Cache-Control: immutable follow-up that makes the version flip pay off. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/proposals/static-asset-cache-busting.md | 113 +++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 114 insertions(+) create mode 100644 docs/proposals/static-asset-cache-busting.md diff --git a/docs/proposals/static-asset-cache-busting.md b/docs/proposals/static-asset-cache-busting.md new file mode 100644 index 00000000..825746ee --- /dev/null +++ b/docs/proposals/static-asset-cache-busting.md @@ -0,0 +1,113 @@ +# Static Asset Cache-Busting Plan + +**Date:** 2026-05-17 +**Status:** Proposed +**Motivation:** Users must hard-reload after every release to pick up new JS/CSS. We need versioned asset URLs that flip automatically on deploy so browsers refetch without manual intervention. + +--- + +## Existing Infrastructure (Already in Place) + +- `app/core/build_info.py` reads `.build-info` (written by `scripts/deploy.sh`) for the deploy commit SHA, falling back to `git rev-parse --short=8 HEAD` for local dev. +- `app/templates_config.py:102-105` exposes `config.commit` and `config.deployed_at` as Jinja globals. +- All static refs use the `url_for('_static', path='...')` pattern — **143 occurrences** across templates. + +The version source is solved. The remaining work is: surface it on every static URL, and let browsers cache aggressively. + +--- + +## Phase 1 — Versioned URL helper (~30 min) + +Add one Jinja global in `app/templates_config.py`: + +```python +def _asset_version() -> str: + info = get_build_info() + return info.get("commit") or info.get("deployed_at", "dev")[:14] + +def static_v(request, name: str, *, path: str) -> str: + return f"{request.url_for(name, path=path)}?v={_asset_version()}" + +templates.env.globals["static_v"] = static_v +``` + +Templates change from: + +```jinja + +``` + +to: + +```jinja + +``` + +`request` is already in scope in every Jinja2Templates render. No new params to thread. + +--- + +## Phase 2 — Migrate the 143 callsites (~1 hr) + +Codemod sweep. Decision point: **bust everything, or only JS/CSS?** + +**Recommendation: JS/CSS only.** Images/fonts/icons rarely change in ways that matter mid-session, and the migration is smaller. Keep raw `url_for` for those. + +Regex pattern (scoped to `.js` and `.css`): + +``` +url_for\(('[a-z_]+_static'), path=('[^']+\.(?:js|css))'\) +→ static_v(request, \1, path=\2) +``` + +Add an architecture rule `FE-008` that warns on raw `url_for('*_static', path='*.js'|'*.css')` to prevent drift. Severity: **warning** initially, **error** once we've cleared the codebase. + +--- + +## Phase 3 — Server-side aggressive caching (~15 min) + +With the version in the query string, static responses can cache forever: + +```python +StaticFiles(directory=..., html=False) +# Wrap with middleware adding: +# Cache-Control: public, max-age=31536000, immutable +# for /static/... paths +``` + +This is the payoff. Without it, the version query string only forces a conditional GET; with it, the browser doesn't even hit the server until the version flips. + +--- + +## Phase 4 — Verification + +- Manual: hard-reload → soft-reload → confirm 304 or memory-cache. +- Smoke test: render any template, assert script tags contain `?v=`. +- Sanity: `scripts/deploy.sh` writes `.build-info` on every deploy (already does — confirm in CI). + +--- + +## Open Decisions + +1. **Bust everything, or just JS/CSS?** → Recommend JS/CSS only. +2. **Version source: commit SHA or deploy timestamp?** → SHA primary, timestamp fallback (already implemented by `build_info.py`). +3. **All 143 callsites in one PR, or incremental?** → All at once via codemod. Cleaner than maintaining two styles. +4. **`FE-008` severity at introduction?** → Warning, escalate to error after migration. + +--- + +## Files Touched + +- `app/templates_config.py` — add `static_v` helper. +- `app/templates/**/*.html` and `app/modules/*/templates/**/*.html` — codemod (JS/CSS refs only). +- `app/main.py` (or wherever `StaticFiles` is mounted) — add cache headers middleware. +- `.architecture-rules/frontend.yaml` (or equivalent) — `FE-008` rule. +- `docs/development/troubleshooting.md` — note the new `?v=` query string in case it confuses anyone debugging static loading. + +--- + +## Out of Scope + +- Per-file hashed filenames (Webpack-style). Not worth the build complexity given Jinja-rendered templates. +- CDN integration. Same `?v=` strategy works behind a CDN; revisit if/when one is introduced. +- Service worker caching. None today; revisit if PWA support is added. diff --git a/mkdocs.yml b/mkdocs.yml index 9170b638..81e2c76b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -351,6 +351,7 @@ nav: - RBAC Cleanup Two-Phase Plan: proposals/rbac-cleanup-two-phase-plan.md - Security Audit Demo POC Builder: proposals/security-audit-demo-poc-builder.md - Security Hardening Plan: proposals/security-hardening-plan.md + - Static Asset Cache-Busting: proposals/static-asset-cache-busting.md - Store Login Platform Detection: proposals/store-login-platform-detection.md - Store Menu Multi-Platform Visibility: proposals/store-menu-multi-platform-visibility.md - Storefront Builder Vision: proposals/storefront-builder-vision.md