diff --git a/nginx.coolify-frontend.conf b/nginx.coolify-frontend.conf index 8941c12f..064a033d 100644 --- a/nginx.coolify-frontend.conf +++ b/nginx.coolify-frontend.conf @@ -8,6 +8,14 @@ server { add_header Content-Security-Policy "frame-ancestors 'self'" always; add_header X-Frame-Options "SAMEORIGIN" always; + location ~ ^/(?:.+/)?(?(?:assets/)?manifest\.webmanifest)$ { + types { application/manifest+json webmanifest; } + default_type application/manifest+json; + add_header Content-Security-Policy "frame-ancestors 'self'" always; + add_header X-Frame-Options "SAMEORIGIN" always; + try_files /$webmanifest_path =404; + } + location ~ ^/(release-entry|release-manifest)\.json$ { add_header Cache-Control "no-store"; add_header Content-Security-Policy "frame-ancestors 'self'" always; diff --git a/public/.htaccess b/public/.htaccess index b8a6f6cf..8db24b7b 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -2,6 +2,10 @@ Options -MultiViews + + AddType application/manifest+json .webmanifest + + RewriteEngine On diff --git a/tests/e2e/release/public-htaccess.local-prod.spec.ts b/tests/e2e/release/public-htaccess.local-prod.spec.ts index a5c1c8c1..266e8f4a 100644 --- a/tests/e2e/release/public-htaccess.local-prod.spec.ts +++ b/tests/e2e/release/public-htaccess.local-prod.spec.ts @@ -284,6 +284,7 @@ test.describe("public .htaccess static fallback", () => { expect(source).not.toContain("/index.html"); expect(source).toContain("index.html [L]"); expect(source).toContain("R=404"); + expect(source).toContain("AddType application/manifest+json .webmanifest"); for (const token of [...STATIC_DIRECTORIES, ...STATIC_FILES, "workbox-"]) { expect(sourceWithoutRegexEscapes).toContain(token); @@ -297,6 +298,7 @@ test.describe("public .htaccess static fallback", () => { expect(source).toContain("static_asset_path"); expect(source).toContain("static_file_path"); expect(source).toContain("=404"); + expect(source).toContain("application/manifest+json webmanifest"); for (const token of [...STATIC_DIRECTORIES, ...STATIC_FILES, "workbox-"]) { expect(sourceWithoutRegexEscapes).toContain(token); @@ -341,6 +343,29 @@ test.describe("public .htaccess static fallback", () => { } }); + test("does not emit conflicting Workbox precache URLs", async () => { + const serviceWorker = await fs.readFile(path.join(DIST_DIR, "sw.js"), "utf8"); + const precacheEntries = Array.from(serviceWorker.matchAll(/\{url:"([^"]+)",revision:(null|"[^"]+")/g), (match) => ({ + url: match[1], + revision: match[2], + })); + const revisionsByUrl = new Map>(); + + for (const entry of precacheEntries) { + const revisions = revisionsByUrl.get(entry.url) || new Set(); + revisions.add(entry.revision); + revisionsByUrl.set(entry.url, revisions); + } + + const conflictingUrls = Array.from(revisionsByUrl.entries()) + .filter(([, revisions]) => revisions.size > 1) + .map(([url]) => url); + + expect(precacheEntries.length).toBeGreaterThan(0); + expect(conflictingUrls).toEqual([]); + expect(precacheEntries.filter((entry) => entry.url === "assets/manifest.webmanifest")).toHaveLength(1); + }); + test("serves the app shell from root, deep links, and release-prefixed paths", async ({ request }) => { for (const appPath of [ "/", diff --git a/vite.config.js b/vite.config.js index 8a16e7aa..f1782e0b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -554,8 +554,8 @@ export default defineConfig(({ mode }) => { cleanupOutdatedCaches: true, skipWaiting: true, clientsClaim: true, - globPatterns: ['**/*.{js,css,html,ico,svg,png,webmanifest,woff2}'], - globIgnores: ['**/registerSW.js', '**/sw.js'], + globPatterns: ['**/*.{js,css,html,ico,svg,png,woff2}'], + globIgnores: ['**/registerSW.js', '**/sw.js', '**/*.webmanifest'], runtimeCaching: [ { // cache API responses