diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts index 263ab40..6ac3ddc 100644 --- a/frontend/src/game/sounds.ts +++ b/frontend/src/game/sounds.ts @@ -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() diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6767bdc..2fca96b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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, diff --git a/server/src/app.ts b/server/src/app.ts index b2ef69b..50efaae 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -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')