diff --git a/.changeset/pwa-https-cold-start-hotfix.md b/.changeset/pwa-https-cold-start-hotfix.md new file mode 100644 index 000000000..e024dca25 --- /dev/null +++ b/.changeset/pwa-https-cold-start-hotfix.md @@ -0,0 +1,5 @@ +--- +'@app/ratewise': patch +--- + +修復 PWA 冷啟動時 Chrome 顯示「此連結並不安全」警告:manifest 改為絕對 HTTPS scope,且 Service Worker 不再快取舊版 manifest。 diff --git a/apps/ratewise/public/manifest.webmanifest b/apps/ratewise/public/manifest.webmanifest index 90f7c30ab..63336ef2d 100644 --- a/apps/ratewise/public/manifest.webmanifest +++ b/apps/ratewise/public/manifest.webmanifest @@ -5,7 +5,7 @@ "theme_color": "#8B5CF6", "background_color": "#E8ECF4", "display": "standalone", - "scope": "/ratewise/", + "scope": "https://app.haotool.org/ratewise/", "start_url": "https://app.haotool.org/ratewise/", "id": "/ratewise/", "orientation": "portrait-primary", diff --git a/apps/ratewise/scripts/generate-manifest.mjs b/apps/ratewise/scripts/generate-manifest.mjs index 4e425fbd8..6fe2fe52f 100644 --- a/apps/ratewise/scripts/generate-manifest.mjs +++ b/apps/ratewise/scripts/generate-manifest.mjs @@ -24,9 +24,9 @@ const manifest = { theme_color: '#8B5CF6', background_color: '#E8ECF4', display: 'standalone', - scope: '/ratewise/', - // 絕對 HTTPS start_url:避免獨立 PWA partition + Chrome HTTPS-First 在啟動時以 http 語意解析。 - // id/scope 維持相對(id 變更會被視為新 PWA 身分,破壞既有安裝更新連續性)。 + // 絕對 HTTPS scope/start_url:避免獨立 PWA partition + Chrome HTTPS-First 在啟動時以 http 語意解析。 + // id 維持相對(id 變更會被視為新 PWA 身分,破壞既有安裝更新連續性)。 + scope: APP_INFO.siteUrl, start_url: APP_INFO.siteUrl, id: '/ratewise/', orientation: 'portrait-primary', diff --git a/apps/ratewise/src/__tests__/sw.test.ts b/apps/ratewise/src/__tests__/sw.test.ts index 4151362e5..616faad01 100644 --- a/apps/ratewise/src/__tests__/sw.test.ts +++ b/apps/ratewise/src/__tests__/sw.test.ts @@ -242,6 +242,17 @@ describe('Service Worker Cache Strategies', () => { expect(sourceCode).toContain('new NetworkOnly('); }); + it('should fetch manifest.webmanifest with NetworkOnly to avoid stale relative start_url', async () => { + const fs = await import('node:fs/promises'); + const path = await import('node:path'); + + const swPath = path.resolve(__dirname, '../sw.ts'); + const sourceCode = await fs.readFile(swPath, 'utf-8'); + + expect(sourceCode).toContain("url.pathname.endsWith('.webmanifest')"); + expect(sourceCode).not.toContain('.(webmanifest|txt|xml)$'); + }); + // 🔴 RED: JS/CSS 應使用 CacheFirst(Vite hash-based filenames 是 immutable) it('should use CacheFirst for JS/CSS static resources (hash-based filenames are immutable)', async () => { const fs = await import('node:fs/promises'); diff --git a/apps/ratewise/src/config/__tests__/build-scripts.test.ts b/apps/ratewise/src/config/__tests__/build-scripts.test.ts index b18add3d0..07d247801 100644 --- a/apps/ratewise/src/config/__tests__/build-scripts.test.ts +++ b/apps/ratewise/src/config/__tests__/build-scripts.test.ts @@ -260,6 +260,21 @@ describe('ratewise build scripts', () => { expect(manifestGenerator).not.toContain("name: 'HaoRate 匯率好工具'"); }); + it('should keep PWA manifest scope and start_url on absolute HTTPS SSOT', async () => { + const manifestGenerator = await readManifestGenerator(); + const manifestPath = path.resolve(__dirname, '../../../public/manifest.webmanifest'); + const manifest = JSON.parse(await readFile(manifestPath, 'utf-8')) as { + scope: string; + start_url: string; + }; + + expect(manifestGenerator).toContain('scope: APP_INFO.siteUrl'); + expect(manifestGenerator).toContain('start_url: APP_INFO.siteUrl'); + expect(manifest.scope).toMatch(/^https:\/\//); + expect(manifest.start_url).toMatch(/^https:\/\//); + expect(manifest.scope).toBe(manifest.start_url); + }); + it('should not force React ecosystem packages into manual chunks', async () => { const viteConfig = await readViteConfig(); expect(viteConfig).not.toContain( diff --git a/apps/ratewise/src/sw.ts b/apps/ratewise/src/sw.ts index 22c4261d7..4e1149f21 100644 --- a/apps/ratewise/src/sw.ts +++ b/apps/ratewise/src/sw.ts @@ -457,9 +457,12 @@ registerRoute( }), ); -// manifest / SEO 文字檔案:StaleWhileRevalidate,7 天。 +// manifest 必須 NetworkOnly:SWR 快取會回傳舊版相對 start_url,觸發冷啟動 HTTPS-First 警告。 +registerRoute(({ url }: { url: URL }) => url.pathname.endsWith('.webmanifest'), new NetworkOnly()); + +// SEO 文字/XML:StaleWhileRevalidate,7 天。 registerRoute( - ({ url }: { url: URL }) => /\.(webmanifest|txt|xml)$/.test(url.pathname), + ({ url }: { url: URL }) => /\.(txt|xml)$/.test(url.pathname), new StaleWhileRevalidate({ cacheName: 'seo-files-cache', plugins: [ diff --git a/docs/dev/002_development_reward_penalty_log.md b/docs/dev/002_development_reward_penalty_log.md index bc38b5da5..a855d3447 100644 --- a/docs/dev/002_development_reward_penalty_log.md +++ b/docs/dev/002_development_reward_penalty_log.md @@ -2,7 +2,7 @@ > 版本:outline-v2-ultra > 原則:每筆只保留日期、ID、原因、解法。 -> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+79 +> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+80 ## 新增模板(4 行) @@ -13,6 +13,11 @@ ## 條目(新→舊) +- 日期:2026-06-28 +- ID:reward-pwa-https-cold-start-manifest-regression +- 原因:#447 已改絕對 HTTPS start_url,但 SW seo-files-cache 以 SWR 快取舊 manifest(相對 start_url),冷啟動仍觸發 HTTPS-First 警告 +- 解法:manifest.webmanifest 改 NetworkOnly、scope 同步絕對 HTTPS SSOT,並補防回歸測試 + - 日期:2026-06-28 - ID:reward-threads-barcelona-inapp-ua - 原因:Threads 2024+ 內建瀏覽器 UA 改用 Barcelona 代號,僅匹配 Threads 字串時 PWA 安裝指引與右上角動畫不顯示