{% 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 %}