Versioning and Cache Busting¶
HyperDjango provides a layered asset versioning system that ensures clients always load the correct version of static files, detects stale-client conditions during HTMX-boosted navigation, and supports deployment strategies like blue/green and canary releases.
Architecture¶
Three layers, each building on the one below:
-
Per-file content hashing — each static file gets a content-hash filename (
styles.a1b2c3d4e5f6.css) viaManifestStaticFilesStorage. Files with hashed names are cached for one year withCache-Control: immutable. -
App-level version — a single hash derived from all static assets (the manifest
hashfield) or explicitly set. Exposed asX-App-Versionheader, template global, and/versionendpoint. -
Version routing — clients send their expected version in a request header; middleware routes to the correct backend or signals a mismatch.
Quick Start¶
from hyperdjango import HyperApp
from hyperdjango.standalone_middleware import VersionMiddleware
app = HyperApp(title="MyApp", static="static")
# Add version header + HTMX mismatch detection
app.use(VersionMiddleware())
# Mount /version endpoint + /cache/bust
app.mount_version()
In templates:
{# Production: content-hash filename (already cache-safe) #}
<link rel="stylesheet" href="{{ static('css/styles.css') }}" />
{# Dev mode: appends ?v=hash from file content #}
<link rel="stylesheet" href="{{ static_url('css/styles.css') }}" />
{# App version for manual use #}
<meta name="app-version" content="{{ app_version() }}" />
Development Mode¶
In development (no collectstatic), the static_url() template helper
appends a ?v=<content_hash> query parameter computed from the file's actual
bytes:
When the file content changes, the hash changes, so browsers fetch the new version. An mtime-based cache avoids re-hashing on every template render.
Controlled by STATIC_DEV_VERSION_QUERY (default: True).
Production Mode¶
Run collectstatic to generate content-hash filenames and a manifest:
This produces:
staticfiles/css/styles.a1b2c3d4e5f6.css— hashed filenamestaticfiles/staticfiles.json— manifest mapping originals to hashed names
The manifest includes a top-level hash field derived from all file hashes.
This becomes the app-level version automatically.
Both {{ static('path') }} and {{ static_url('path') }} resolve to the
hashed filename in production.
X-App-Version Header¶
When APP_VERSION_HEADER is True (default), every HTTP response includes:
The version is resolved in this order:
- Explicit
APP_VERSIONsetting (git SHA, semver, etc.) - Manifest
hashfromstaticfiles.json - Computed hash from registered component files
- Fallback:
"unknown"
HTMX Version Mismatch Detection¶
HTMX-boosted navigation replaces page content without reloading the full page. If the server deploys a new version mid-session, the client's JavaScript and CSS become stale.
VersionMiddleware injects a script into HTML responses that listens for
htmx:afterRequest events and compares the X-App-Version response header
against the page's window.__hyperAppVersion. On mismatch:
"reload"(default) — forceswindow.location.reload()"warn"— logs toconsole.warn()without reloading"ignore"— disables script injection entirely
Controlled by APP_VERSION_MISMATCH setting.
Version Endpoint¶
app.mount_version() registers two endpoints:
GET /version¶
Returns app version metadata:
POST /cache/bust¶
Manual cache invalidation for deploy scripts. Requires an auth token:
# Generate token from SECRET_KEY
TOKEN=$(python -c "
from hyperdjango.versioning import _make_cache_bust_token
print(_make_cache_bust_token())
")
curl -X POST https://myapp.com/cache/bust \
-H "Authorization: Bearer $TOKEN"
Response:
Component Registration¶
Register non-static files that contribute to the app version:
app.register_version_component("templates", [
"templates/base.html",
"templates/nav.html",
])
app.register_version_component("config", [
"config/production.json",
])
The app version is derived from both the static manifest hash and the registered component file hashes.
Blue/Green and Canary Deployments¶
VersionRouterMiddleware supports version-based request routing:
from hyperdjango.standalone_middleware import VersionRouterMiddleware
app.use(VersionRouterMiddleware(
version_map={
"v1.0": "backend-blue",
"v1.1": "backend-green",
},
default_version="v1.1",
))
The middleware:
- Reads
X-App-Requested-Versionfrom the request header (orapp_versioncookie) - If the version is in
version_map, setsx-backend-targeton the response - If the version is unknown, returns
409 Conflictwith available versions - The upstream proxy (nginx, Envoy) reads
x-backend-targetfor routing
Nginx example:
location / {
proxy_pass http://app;
proxy_set_header X-App-Requested-Version $http_x_app_requested_version;
# Route based on response header (requires post-processing)
# Or use Lua/njs to read the header and re-route
}
Settings Reference¶
| Setting | Type | Default | Description |
|---|---|---|---|
APP_VERSION |
str | "" |
Explicit version (git SHA, semver). Empty = auto-compute |
APP_VERSION_HEADER |
bool | True |
Emit X-App-Version on all responses |
APP_VERSION_MISMATCH |
str | "reload" |
Client action: "reload", "warn", "ignore" |
VERSION_ENDPOINT |
bool | True |
Mount /version when mount_version() called |
STATIC_DEV_VERSION_QUERY |
bool | True |
Append ?v=hash in dev mode |
Design Principles¶
These rules should be followed by any subsystem that needs caching or versioning:
-
One manifest, not several unrelated version stores. All version information flows through
AppVersion. -
Version actual bytes. Hash the final emitted content (compiled CSS, minified JS, serialized JSON), not filenames, mtimes, or deployment timestamps.
-
Derive parent version from child versions. The app-level version is derived from per-file hashes, so one freshness signal answers "is the currently loaded runtime still valid for this response?"
-
Rewrite embedded asset refs centrally. Templates write canonical paths (
css/styles.css); the renderer injects versioned URLs. Authors never hardcode version query params into source content. -
Manual bust APIs are authoritative. Cache refreshes happen after coherent operator-triggered mutations (deploys), not during half-finished edits or via background file watchers.
-
Distinguish canonical paths from cacheable paths. Canonical path = stable identifier. Versioned path = cacheable delivery path.
-
Make stale-client detection explicit. If partial-page navigation exists (HTMX), emit a version header and reload on mismatch.