fix: stale SW chunks serving HTML, PWA cache cleanup, server asset routing

- Server SPA fallback now returns 404 for missing .js/.css assets instead of index.html
- Prevents "Unexpected token '<'" errors from stale chunk URLs
- Serve PWA files (sw.js, workbox, icons) with correct cache headers
- Workbox: cleanupOutdatedCaches, skipWaiting, clientsClaim for instant SW updates
- navigateFallbackDenylist for /api/ routes
- AudioContext comment clarification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 23:08:25 +00:00
co-authored by Claude Opus 4.6
parent 20e9f0f7a2
commit 7d5f4a6b8d
3 changed files with 20 additions and 1 deletions
+3
View File
@@ -5,6 +5,9 @@ let sfxGain: GainNode | null = null
let musicPlaying = false
let musicTimeout: number | null = null
// Lazily create AudioContext + gain nodes on first use.
// Chrome may warn about autoplay policy but the context will resume once ensureAudioContext()
// is called from a user gesture. All SFX calls before that are queued/silent.
function getCtx(): AudioContext {
if (!ctx) {
ctx = new AudioContext()
+4
View File
@@ -28,7 +28,11 @@ export default defineConfig({
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,woff,woff2}'],
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
navigateFallback: '/index.html',
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
+13 -1
View File
@@ -75,9 +75,21 @@ if (process.env.NODE_ENV === 'production' && existsSync(publicDir)) {
app.get('/robots.txt', (c) => serveFile(c, '/robots.txt', 'public, max-age=86400'))
app.get('/manifest.webmanifest', (c) => serveFile(c, '/manifest.webmanifest', 'public, max-age=86400'))
// SPA fallback: all non-API routes serve index.html
// PWA service worker + related root files
app.get('/sw.js', (c) => serveFile(c, '/sw.js', 'no-cache'))
app.get('/workbox-*.js', (c) => serveFile(c, c.req.path, 'public, max-age=31536000, immutable'))
app.get('/icon-*.png', (c) => serveFile(c, c.req.path, 'public, max-age=86400'))
app.get('/icon.svg', (c) => serveFile(c, '/icon.svg', 'public, max-age=86400'))
app.get('/apple-touch-icon.png', (c) => serveFile(c, '/apple-touch-icon.png', 'public, max-age=86400'))
// SPA fallback: only for navigation requests (not JS/CSS/asset files)
app.get('*', (c) => {
if (c.req.path.startsWith('/api/')) return c.notFound()
// Don't serve index.html for asset requests — return 404 so the browser gets a proper error
const ext = c.req.path.split('.').pop()
if (ext && ext !== c.req.path && ['js', 'css', 'map', 'json', 'png', 'jpg', 'svg', 'woff', 'woff2', 'webp', 'ico'].includes(ext)) {
return c.notFound()
}
const indexPath = join(publicDir, 'index.html')
c.header('Content-Type', 'text/html')
c.header('Cache-Control', 'no-cache')