diff --git a/docs/web.md b/docs/web.md
index 623b6dd9..104cbc2c 100644
--- a/docs/web.md
+++ b/docs/web.md
@@ -31,9 +31,11 @@ isolated like a production deployment should be. Any static server works
(`python3 -m http.server -d target/web/dist 8765`); it must serve `.wasm` as `application/wasm`.
Service workers need a secure context: `https://`, or `http://localhost` / `127.0.0.1`.
-The release `.wasm` is about 38 MB before `wasm-opt` (code: the effects, codecs, the expression
-engine and the UI; data, mostly the bundled fonts; function names for readable panics). Served
-compressed it is a fraction of that, and the service worker caches it after the first visit.
+The release `.wasm` is about 58 MB before `wasm-opt` (v0.3.1; code: the effects, codecs, the
+expression engine and the UI; data, mostly the bundled fonts; function names for readable
+panics). Served compressed it is about 17 MB with gzip or 10 MB with Brotli, and the service
+worker caches it after the first visit. Its name doesn't change between releases, so hosts
+revalidate it rather than caching it for long (`packaging/web/README.md`).
On other targets the web crate holds only its portable storage model (`store.rs`, unit-tested by
`cargo test --workspace`); `cargo xtask wasm` (part of `cargo xtask ci`) checks the whole crate,
diff --git a/packaging/web/.htaccess b/packaging/web/.htaccess
index 7b7e6e18..60f128d3 100644
--- a/packaging/web/.htaccess
+++ b/packaging/web/.htaccess
@@ -7,10 +7,8 @@ AddType text/javascript .js
-
- Header set Cache-Control "public, max-age=31536000, immutable"
-
-
+ # The file names don't change between releases: revalidate them (HOSTING.md).
+
Header set Cache-Control "no-cache"
diff --git a/packaging/web/README.md b/packaging/web/README.md
index 230525ba..0a1f56fe 100644
--- a/packaging/web/README.md
+++ b/packaging/web/README.md
@@ -6,29 +6,38 @@ static site in `effectcraft-web-/`:
| File | What it is |
|---|---|
| `index.html` | The page. It loads everything through relative URLs. |
-| `effectcraft-web-.js` | wasm-bindgen glue (generated, ES module) |
-| `effectcraft-web-_bg.wasm` | The app, about 13 MB, or 5 MB with compression |
+| `effectcraft_web.js` | wasm-bindgen glue (generated, ES module) |
+| `effectcraft_web_bg.wasm` | The app: about 58 MB, about 17 MB with gzip and 10 MB with Brotli (v0.3.1) |
+| `snippets/…/js/host.js` | Browser glue (storage, audio, workers) |
+| `worker.js`, `audio-worklet.js` | Render and frame workers, and audio playback |
+| `sw.js`, `manifest.webmanifest`, icons | Offline install: the service worker and the app manifest |
| `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache |
There is no server-side code. Upload the folder's contents anywhere that serves static files.
## Any path works
-All URLs in `index.html` are relative (`public_url = "./"` in `apps/effectcraft-web/Trunk.toml`),
-so the site works at a domain root (`https://example.com/`), under a prefix
-(`https://example.com/tools/effectcraft/`) and from a CDN bucket. The asset names carry a content
-hash, so they can be cached forever. Only `index.html` needs revalidation.
+All URLs in `index.html` are relative: the page is written that way
+(`apps/effectcraft-web/web/index.html`), and `package.sh` refuses a build with root-absolute
+URLs. So the site works at a domain root (`https://example.com/`), under a prefix
+(`https://example.com/tools/effectcraft/`) and from a CDN bucket.
+
+The file names are the same in every release (`effectcraft_web_bg.wasm` and so on), so browsers
+must revalidate them: a long-lived cache would keep returning visitors on the old version after
+you upgrade in place. Revalidation is cheap (an unchanged file answers `304 Not Modified`), and
+the service worker keeps the app for offline use after the first visit. To cache the files for
+longer, serve each release from its own versioned path (`/effectcraft/0.4.0/`).
## Required server settings
- **MIME type:** serve `.wasm` as `application/wasm`. Browsers refuse to stream-compile it under
any other type, and the app then loads slowly or not at all. Serve `.js` as `text/javascript`.
Most hosts already do both. For nginx, check that `mime.types` has `application/wasm wasm;`.
-- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. That takes the
- download from about 13 MB to about 5 MB. You can also precompress (`brotli -k *.wasm`) and let
- the server send `Content-Encoding: br`.
-- **Caching:** `Cache-Control: public, max-age=31536000, immutable` on the hashed `.wasm` and
- `.js` files, and `no-cache` on `index.html`.
+- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. For v0.3.1 that takes
+ the download from about 58 MB to about 17 MB (gzip) or 10 MB (Brotli). You can also
+ precompress (`brotli -k *.wasm`) and let the server send `Content-Encoding: br`.
+- **Caching:** `Cache-Control: no-cache` (revalidate) on `index.html`, `sw.js` and the `.wasm`
+ and `.js` files, since their names don't change between releases (see above).
- **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://`
or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2.
- **No special isolation headers:** EffectCraft doesn't use `SharedArrayBuffer`, so it doesn't
@@ -43,8 +52,7 @@ location /effectcraft/ {
types { application/wasm wasm; text/javascript js; text/html html; }
gzip on;
gzip_types application/wasm text/javascript text/html;
- location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
- location ~* index\.html$ { add_header Cache-Control "no-cache"; }
+ location ~* (\.(wasm|js)|index\.html)$ { add_header Cache-Control "no-cache"; }
}
```
@@ -55,7 +63,7 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo
```html