Files
orion/docs/proposals/static-asset-cache-busting.md
Samir Boulahtit e680fda8bd
Some checks failed
CI / ruff (push) Successful in 16s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
docs(proposals): static asset cache-busting plan
Proposes a one-liner Jinja helper that appends ?v=<commit-sha> 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) <noreply@anthropic.com>
2026-05-17 22:35:24 +02:00

4.0 KiB

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('<module>_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:

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:

<script src="{{ url_for('dev_tools_static', path='admin/js/sql-query.js') }}"></script>

to:

<script src="{{ static_v(request, 'dev_tools_static', path='admin/js/sql-query.js') }}"></script>

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:

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.