38686-vm/core/context_processors.py
Konrad du Plessis 16d4399c28 perf(cache): mtime-based CSS cache-bust token
deployment_timestamp was int(time.time()) per-request, giving every
page load a new ?v=... query string on custom.css. Cloudflare treats
each unique URL as a new resource, so the CSS was fetched from the VM
on every page load — 64 KB over the wire per navigation.

Token now tied to static/css/custom.css mtime. The URL only changes
when the CSS actually changes, so Cloudflare can hold the file for
its full 4h TTL. Degraded-mode fallback preserves today's behaviour
if the file isn't on disk.

3 new CacheBustTokenTests; all 68 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:31:05 +02:00

45 lines
1.8 KiB
Python

# === core/context_processors.py ===
# Globals injected into every template render.
#
# `deployment_timestamp` is a cache-bust token on our CSS URL. Historically
# it was `int(time.time())` — a new value every second — which defeated
# Cloudflare's edge cache because every page load saw a different `?v=...`
# string. We now tie the token to `custom.css`'s mtime, so the URL
# changes ONLY when the CSS actually changes. Cloudflare can hold the
# file for its full 4h TTL, and users on repeat visits hit the browser
# cache (304 Not Modified).
import os
import time
from pathlib import Path
from django.conf import settings
# Path to the file whose mtime drives the cache-bust token. Module-level
# constant so tests can monkey-patch it to simulate "file missing".
_CSS_PATH_FOR_TOKEN = Path(settings.BASE_DIR) / 'static' / 'css' / 'custom.css'
def _compute_cache_bust_token():
"""Return an integer cache-bust token.
Normal path: returns the CSS file's mtime (an integer).
Fallback: if the file can't be stat'd (doesn't exist / permission
error / disk issue), returns the current wall-clock time in seconds
— that degrades to the PRE-FIX behaviour (new token per request)
rather than crashing the whole request cycle.
"""
try:
return int(os.path.getmtime(_CSS_PATH_FOR_TOKEN))
except (OSError, FileNotFoundError):
return int(time.time())
def project_context(request):
"""Adds project-specific environment variables to the template context globally."""
return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
# Cache-busts static assets — see _compute_cache_bust_token().
"deployment_timestamp": _compute_cache_bust_token(),
}