Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pwa-https-cold-start-hotfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

修復 PWA 冷啟動時 Chrome 顯示「此連結並不安全」警告:manifest 改為絕對 HTTPS scope,且 Service Worker 不再快取舊版 manifest。
2 changes: 1 addition & 1 deletion apps/ratewise/public/manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions apps/ratewise/scripts/generate-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions apps/ratewise/src/__tests__/sw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
15 changes: 15 additions & 0 deletions apps/ratewise/src/config/__tests__/build-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions apps/ratewise/src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
7 changes: 6 additions & 1 deletion docs/dev/002_development_reward_penalty_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 行)

Expand All @@ -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 安裝指引與右上角動畫不顯示
Expand Down
Loading