diff --git a/.changeset/manifest-site-url-env.md b/.changeset/manifest-site-url-env.md new file mode 100644 index 000000000..a5c6acaf2 --- /dev/null +++ b/.changeset/manifest-site-url-env.md @@ -0,0 +1,5 @@ +--- +'@app/ratewise': patch +--- + +staging 等替代網域安裝 PWA 時 manifest scope 不再指向正式站:scope/start_url 改由 VITE_SITE_URL 環境變數驅動(含 trailing-slash 正規化),未設定時輸出與正式站完全相同。 diff --git a/apps/ratewise/scripts/generate-manifest.mjs b/apps/ratewise/scripts/generate-manifest.mjs index 050c41ee3..b2b839181 100644 --- a/apps/ratewise/scripts/generate-manifest.mjs +++ b/apps/ratewise/scripts/generate-manifest.mjs @@ -30,6 +30,16 @@ const currencyCount = [...constantsContent.matchAll(/^\s+([A-Z]{3}):\s*\{/gm)].l const versioned = (path) => `${path}?v=${VERSION_TOKEN}`; +// scope/start_url 以 VITE_SITE_URL 環境變數驅動(與 seo-metadata canonical 同一 SSOT), +// 未設定時回退正式站 APP_INFO.siteUrl;staging 等替代網域部署設定該變數即可,禁止硬編網域。 +// 正規化行為對齊 seo-paths.ts 的 normalizeSiteUrl(trim+補尾斜線);無法直接 import 複用: +// seo-paths.ts 內部以無副檔名路徑 import './app-info',Node Type Stripping 不解析副檔名。 +function normalizeSiteUrl(value) { + const trimmed = value.trim(); + return trimmed.endsWith('/') ? trimmed : `${trimmed}/`; +} +const siteUrl = normalizeSiteUrl(process.env.VITE_SITE_URL || APP_INFO.siteUrl); + const manifest = { name: APP_INFO.name, short_name: APP_MANIFEST.shortName, @@ -39,8 +49,8 @@ const manifest = { display: 'standalone', // 絕對 HTTPS scope/start_url:避免獨立 PWA partition + Chrome HTTPS-First 在啟動時以 http 語意解析。 // id 維持相對(id 變更會被視為新 PWA 身分,破壞既有安裝更新連續性)。 - scope: APP_INFO.siteUrl, - start_url: APP_INFO.siteUrl, + scope: siteUrl, + start_url: siteUrl, id: '/ratewise/', orientation: 'portrait-primary', categories: ['finance', 'utilities', 'productivity'], diff --git a/apps/ratewise/src/config/__tests__/build-scripts.test.ts b/apps/ratewise/src/config/__tests__/build-scripts.test.ts index 4e29a5d9e..ddb087610 100644 --- a/apps/ratewise/src/config/__tests__/build-scripts.test.ts +++ b/apps/ratewise/src/config/__tests__/build-scripts.test.ts @@ -3,6 +3,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; +import { APP_INFO } from '../app-info'; async function readPackageJson() { const packageJsonPath = path.resolve(__dirname, '../../../package.json'); @@ -270,11 +271,19 @@ describe('ratewise build scripts', () => { start_url: string; }; - expect(manifestGenerator).toContain('scope: APP_INFO.siteUrl'); - expect(manifestGenerator).toContain('start_url: APP_INFO.siteUrl'); + // scope/start_url 由 VITE_SITE_URL 環境變數驅動(staging 等替代網域),未設定時回退正式站 SSOT, + // 正規化必須複用 seo-paths 的 normalizeSiteUrl 避免行為分歧。 + expect(manifestGenerator).toContain( + 'normalizeSiteUrl(process.env.VITE_SITE_URL || APP_INFO.siteUrl)', + ); + expect(manifestGenerator).toContain('scope: siteUrl'); + expect(manifestGenerator).toContain('start_url: siteUrl'); expect(manifest.scope).toMatch(/^https:\/\//); expect(manifest.start_url).toMatch(/^https:\/\//); expect(manifest.scope).toBe(manifest.start_url); + // committed manifest 必為正式站:堵住 env 污染下(VITE_SITE_URL 指向 staging) + // 重新生成並誤 commit staging URL 仍全綠的缺口。 + expect(manifest.scope).toBe(APP_INFO.siteUrl); }); it('should not force React ecosystem packages into manual chunks', async () => { diff --git a/docs/dev/002_development_reward_penalty_log.md b/docs/dev/002_development_reward_penalty_log.md index 2b04bf7b1..1124afbae 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、原因、解法。 -> 本次分數變化:+2(reward 2、penalty 0、neutral 1)|累計總分:+149 +> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+150 ## 新增模板(4 行) @@ -13,6 +13,11 @@ ## 條目(新→舊) +- 日期:2026-07-06 +- ID:reward-rw-602-manifest-site-url-env +- 原因:generate-manifest 的 scope/start_url 硬編 APP_INFO.siteUrl(app.haotool.org),staging 等替代網域部署時 manifest scope 跨網域、PWA 安裝行為異常(#602;字體 @font-face 被 Beasties 修剪部分經 PM 裁決移交 #616 處理) +- 解法:scope/start_url 改由 VITE_SITE_URL 環境變數驅動+normalizeSiteUrl 正規化(行為對齊 seo-paths;直接 import 因該檔無副檔名 import 在 Node Type Stripping 下 ERR_MODULE_NOT_FOUND,以註解標明),未設定時回退正式站;committed manifest 補「必為正式站」JSON 斷言堵 env 污染誤 commit,generate:deterministic 冪等零 diff 實證 + - 日期:2026-07-06 - ID:reward-rw-600-anchor-scroll-mt-safe-area - 原因:#605 修了 sticky 返回列 safe-area 但錨點目標 `scroll-mt-20` 固定 80px 未連動,standalone 下 sticky 列變高(+59px)後隱私目錄與 Guide 步驟跳轉被遮蔽(#600 連動項)