{% raw %} # Static asset caching and versioning DevPlace serves its CSS and JavaScript with a one-year immutable cache for the best possible "Serve static assets with an efficient cache policy" score, while still letting a deploy take effect immediately. The two goals are reconciled by stamping a **boot-time version** into every app-owned static URL. See also [nginx and networking](/docs/production-nginx.html) and [Deploy and update](/docs/production-deploy.html). ## How it works Every static URL the app emits carries a version path segment: ``` /static/css/base.css -> /static/v1718040000/css/base.css /static/js/Application.js -> /static/v1718040000/js/Application.js ``` `v1718040000` is the unix timestamp captured once when the server process starts (`config.STATIC_VERSION`). Because the value is fixed for the life of the process: - **Heavy caching is safe.** Each versioned URL is unique per deploy, so the browser may cache it for a full year (`Cache-Control: public, immutable, max-age=31536000`). - **Deploys are instant.** Restarting the server changes the boot timestamp, so every asset URL changes, so every returning browser refetches on its next page load. No manual cache purge, no hashing build step, no waiting for a TTL to expire. ## Why a path segment, not a query string DevPlace ships unbundled ES6 modules wired together with **relative imports** (`import { Http } from "./Http.js"`). A relative import resolves against the URL of the module that contains it. If only the entry ` ``` It returns the path unchanged for anything outside `/static/` and for `/static/uploads/` (user content, addressed by stable database paths), so it is safe to wrap around dynamic values such as `{{ static_url(og_image or '/static/og-default.png') }}`. - **JavaScript** that builds an absolute static URL at runtime (component CSS link injection, lazily loaded xterm/md-clippy modules) uses the `assetUrl` helper (`static/js/assetVersion.js`), which reads the version from the `` tag rendered in `base.html`: ``` import { assetUrl } from "../assetVersion.js"; link.href = assetUrl("/static/css/components.css"); ``` Relative module imports never need either helper - they inherit the version from the importing module's URL. ## Serving and cache headers | Layer | Versioned `/static/v/...` | Unversioned `/static/...` | |-------|------------------------------|---------------------------| | App (dev / `make dev`) | `CachedStaticFiles` mount sets `public, immutable, max-age=31536000` | plain mount, validator-based revalidation | | nginx (prod) | regex `location ~ ^/static/v\d+/` sets `public, immutable, max-age=31536000` | `public, max-age=3600` (one hour, not immutable) | The unversioned `/static/` location stays a short, non-immutable cache because a few unversioned URLs still exist (the service worker's precache icons, direct hits); they must not be frozen for a year. **Service worker exception.** `service-worker.js` is served with `Cache-Control: no-cache` (by its `/service-worker.js` route and, defensively, by `CachedStaticFiles`) so a new worker always deploys. It keeps a stable, unversioned URL so its registration scope does not change on every deploy. **Uploads exclusion.** `/static/uploads/` is user-generated content with its own public cache and stable stored paths; it is never boot-versioned. ## Multi-worker consistency In production the app runs multiple uvicorn workers. Each worker is a separate process, so if every worker computed its own timestamp they could disagree by a second and emit mismatched asset URLs. The version is therefore fixed **once at launch** and shared through the `DEVPLACE_STATIC_VERSION` environment variable: - `make prod` prefixes the command with `DEVPLACE_STATIC_VERSION=$(date +%s)`. - The Docker image launches uvicorn through `sh -c` so a single `date +%s` is captured and exported before the workers fork. When `DEVPLACE_STATIC_VERSION` is unset (dev with `--reload`), each process boot computes a fresh timestamp, so a reload after editing a file naturally produces a new version. You can also pin the variable to a build id or git sha in CI for reproducible URLs. ## Verify ``` curl -sI http://localhost:10500/static/v$(date +%s)/css/base.css | grep -i cache-control # -> Cache-Control: public, max-age=31536000, immutable curl -sI http://localhost:10500/service-worker.js | grep -i cache-control # -> Cache-Control: no-cache ``` In a rendered page, the `` value and every `/static/v.../` URL change after a server restart. {% endraw %}