Skip to content

PWA

modules: ["pwa"] plus an optional pwa block. Two options matter for an app that isn’t the whole site:

offlineFallback—the page to serve when a navigation fails offline. Without it the fallback is the scope root, that is, the dynamic app shell, which is exactly the wrong thing to precache on an auth-gated app: that response carries Cache-Control: no-store, so either nothing is cached and the fallback is empty, or a signed-in shell is stored and later served to whoever opens the app next.

Point it at a prerendered page with no session-specific markup. It’s precached with the shell—a fallback fetched on demand isn’t there when it’s needed.

emitDir—the subdirectory under public/ to write sw.js and the manifest into. For a PWA on its own subdomain that rewrites to a path prefix (studio.example.com//studio/, see Tenancy), the browser fetches /sw.js at its origin root, which rewrites to /studio/sw.js. Emitted at the public root, that’s a 404 with nothing to explain it.

modules: ["pwa"],
pwa: { scope: "/studio", emitDir: "studio", offlineFallback: "/offline" },

_headers stays at the public root—Cloudflare only reads it there—but its stanza moves with the files, so the no-cache rule that stops a bad service worker sticking around still applies.

With scope equal to the serving path, no Service-Worker-Allowed header is needed: a worker may always control its own directory and below.