parity-gates
從 capture/ 建立基準線一次,然後在每次建置時執行關卡,直到所有關卡都通過。
關卡僅讀取 baseline/,從不讀取即時網站,因此即使舊主機已移除,建置仍可被驗證。
黃金法則
預設建置等於即時網站,且關卡絕不軟化。 沒有旗標能將失敗轉為警告。由此衍生出另外四條:
- 絕不手動編輯
baseline/。 唯一的例外:baseline/scripts.json中的keep,用於決定是否移除某個廠商(設定為false,並記錄此決定)。 - 差異是條目,而非編輯。 不可避免的差異:
exceptions.json。改進項目:proposals.json,僅在PROPOSALS=on時啟用。兩者都在references/exceptions.md中。 - 跳過不等於通過。 無法檢查某項的關卡會在其摘要行中顯示
N skipped (reason)。報告每個跳過項目。 - 錯誤的關卡應作為套件錯誤回報,絕不修改網站中的關卡,也不修改基準線。
執行腳本
由 convert-site 建立的網站會在 scripts/ 中放置腳本;從網站根目錄執行以下 npm 腳本,選項放在 -- 之後。在非範本化網站外部,從網站根目錄執行每個步驟下方顯示的 node 形式,其中 <skill-dir> 是在已執行 npm install 的套件副本中該技能資料夾的絕對路徑。每個腳本都會列印 --help;請執行它而非閱讀原始碼。退出碼 0 為通過,1 為失敗的關卡或漂移,2 為使用方式錯誤或缺少輸入。
1. 基準線(僅一次)
npm run baseline
node <skill-dir>/scripts/build-baseline.mjs --capture capture --out baseline # 備用
它需要 capture/inventory.json、media.json(來自 capture 的 media.mjs)和 templates.json
(templates.mjs),如果缺少其中一個,則會退出碼 2 並命名需執行的步驟。它會寫入
baseline/{routes,seo,redirects,media,forms,scripts,meta,counts}.json 和 baseline/text/,以及
網站根目錄的 redirects.csv。沒有目標的短連結會被省略並命名在 stderr 上。
捕獲鏈中的每個中間跳轉都會獲得一個直接指向最終目標的單獨行
(/old-services → /old-services/ → /services/ 也會產生 /old-services/,/services/,請注意
hop of /old-services),除非該跳轉本身是已建置的路由,因此指向該鏈中任何 URL 的連結都只需一次跳轉。
一起提交 baseline/ 和 redirects.csv。--check 會根據 counts.json 重新計數 baseline/(漂移則退出碼 1)。每個檔案的格式在 references/contracts.md 中。
2. 重新導向
redirects.csv(from,to,status,source,note)是網站重新導向的單一來源。
source 是來自 capture 的 crawl、sitemap、shortlink 或 seed;您新增的一行是
decision,其 note 命名決策 ID。絕不手動編輯編譯後的輸出;編譯方式:
npm run redirects:build
node <skill-dir>/scripts/build-redirects.mjs --csv redirects.csv --host vercel --out vercel.json --trailing-slash always # 備用
--host cloudflare --out public/_redirects 用於 Pages(Astro 會將其複製到 dist/ 中)。Vercel:
redirects 鍵會被取代,所有其他鍵保留,並且 trailingSlash 會被移除,並寫入編譯器自己位於網站規則之後的 308 斜線規則,因此未帶斜線的來源仍然只需一次跳轉。
只有以靜態檔案副檔名結尾的路徑才會被這些規則視為檔案,且不區分大小寫:
網頁類型、文件、媒體和歸檔類型(docx、xlsx、
csv、mov、wav、heic、gz、...),以及 dist/(否則為 public/)中找到的每個副檔名,因此請在
建置後重新執行;/upgrade-guide/3.5/ 是一個頁面,並保留其斜線。規則仍會重新導向的已建置檔案會使重新導向關卡失敗,顯示為 built file redirected。
Cloudflare:查詢行(/?p=7)無法由 _redirects 匹配,並會進入
cloudflare-query-redirects.csv,需要建立為單一重新導向(一個「需要您處理」的項目)。一個
目標是其自身路徑的查詢行(/?p=2 到 /)是無操作的,並且在兩個主機上都會被省略。
捕獲的行(任何 source 但不是 decision)是即時 URL 且不包含參數:對於 Vercel,
其中的每個 : 和 * 都會被跳脫(/foo:bar/ 被寫為 /foo\:bar/,
/trackshttps://www.example.com/x/ 也是如此,這是捕獲所跟隨的損壞即時 href);// 則保留。
只有 decision 行使用 :name 參數和 * splat,即使在這種情況下,數字前的 :(連接埠)也會被跳脫。Vercel 拒絕裸露的 :,關卡的模擬也是如此。
沒有有效格式的來源(沒有前導 /、空白字元、控制字元;在 Cloudflare 上還有捕獲的 :<name> 或 *,_redirects 無法跳脫)會被丟棄,在 stderr 上命名,並在 redirects-skipped.json 中列出為 { source, reason }。該檔案位於 redirects.csv 旁邊(網站根目錄),絕不在 public/ 下,因此不會被部署。重新導向關卡和 verify-live 將列出的行計為跳過,絕不計為通過。當路徑根本不應該有重新導向時,請改為記錄一個「無重新導向」例外(第 4 節):{ "kind": "redirect", "path": "<source>", "reason": "...", "action": "none", "approvedBy": "...", "date": "..." }。
3. 驗證
npm run build
npm run verify -- --endpoint https://forms.example.com/f/abc --thank-you /thank-you/
node <skill-dir>/scripts/verify/run-all.mjs --endpoint https://forms.example.com/f/abc --thank-you /thank-you/ # 備用
run-all 按字母順序執行十個關卡,僅為每個關卡提供它接受的選項,
並在第一個失敗時停止。共用選項:--paths(逗號分隔清單,* 匹配任何內容)、
--json、--origin(連結、SEO、Sitemap:建置所針對的主機)、--allow-extra(路由)、
--endpoint、--honeypot、--thank-you(表單;當基準線中有 POST 表單時,--endpoint 為必要)、--host、--config(重新導向、連結)、--csv(重新導向)、--media-plan(媒體)。修復第一個失敗,
重建,重新執行。單獨一個關卡:node <skill-dir>/scripts/verify/<gate>.mjs。
| 關卡 | 通過條件 | 跳過表示 |
|---|---|---|
forms | 每個即時表單都存在於其頁面上,且欄位名稱相同、順序一致、方法相同;POST 表單的 action 是 --endpoint,並帶有 --honeypot;GET 表單(搜尋框)保留即時的 action 路徑,不需要這兩者(「比較了 N 個搜尋/GET 表單」);即時 action 位於另一個可註冊網域(電子報列表主機;www、http、子網域和 WordPress 處理程式,例如 admin-ajax.php 或 ?wpcf7 皆視為網站)的 POST 表單保持不變,透過其完整 action URL 像 GET 表單一樣進行比較,且不需要 --endpoint(「保留了 N 個表單(發布到 )」) | 無 --thank-you:未檢查 POST 表單的感謝頁面 |
headers | 主機設定在每個路徑上發送 nosniff、Referrer-Policy、X-Frame-Options 或 CSP frame-ancestors,並且當不可索引時,在每個路徑上發送 X-Robots-Tag: noindex 或(Vercel)在每個建置頁面上的 meta robots noindex;預覽主機規則(https://*.pages.dev/*)僅適用於預覽主機;使用 PUBLIC_INDEXABLE=true 時,禁止在每個路徑上使用 noindex | |
links | 每個內部 href 和 src 解析為已建置的頁面或檔案、重新導向來源或基準線媒體,或透過編譯後的規則(vercel.json 或 dist/_redirects:斜線規則和每個 redirects.csv 行)一次跳轉即可到達已建置的頁面,如同即時重新導向一樣(「透過重新導向解析了 N 個連結」);兩次跳轉或缺少目標則失敗;沒有主機設定時,/about(建置提供 /about/)失敗 | |
media | 每個基準線媒體檔案都由建置以相同的位元組提供(--paths 透過路徑或引用它的頁面來選擇檔案):在計劃模式 repo 的 dist/ 中;在計畫的 prefix 下,儲存於 store manifest(store)或仍在即時網站上(live-temporary);在前綴之外,每種模式都在 dist/ 中。使用 PUBLIC_INDEXABLE=true 時,live-temporary 會使前綴下的每個檔案失敗(正式環境建置不得從即時網站提供媒體;沒有前綴的計劃涵蓋每個檔案),並且仍會檢查 dist/ 中前綴外的檔案 | 在暫存建置上的計劃模式 live-temporary:前綴下的檔案從即時網站提供,不進行檢查 |
redirects | 每個基準線重新導向和 csv 行在一次跳轉中回答,且狀態碼相同,並且目標在 dist/ 中回答 200(或是外部的,或是在基準線 meta.json excluded 中的前綴下:則僅檢查第一次跳轉);沒有來源同時也是已建置的檔案;沒有已建置的頁面或檔案被重新導向其自身 URL 之外(built page redirected by rule <rule>;沒有例外涵蓋它,修復規則);具有「無重新導向」例外(kind: "redirect",action: "none")的路徑根本不會被重新導向 | 無操作的查詢行、Cloudflare 上的任何查詢行,或 redirects-skipped.json 列出原因(not compiled)的無有效格式來源 |
routes | 每個頁面、文章、歸檔和 feed 在 200 狀態碼下建置,noindex 路由仍表示 noindex,沒有建置多餘的項目(捕獲在 baseline/js.json 中的自己的腳本和 baseline/css.json 中的樣式表,在其即時路徑上;即時 sitemap 名稱在 baseline/sitemaps.json 中,以及啟動圖示集都不是多餘的);以非 200、3xx、404 或 410 狀態碼捕獲的路由會失敗,顯示為 not captured: <status>,除非 routes 例外跳過它或命名其建置的替代方案 | 帶有 to: "skip" 的 routes 例外:該路由從未被捕獲 |
scripts | 每個保留的廠商腳本正好存在於其即時頁面上,且 ID 相同;已移除的廠商腳本不存在於任何頁面上;沒有新的廠商腳本 | |
seo | 每個頁面的 Title、Description、Canonical、Robots、Lang、H1、H2、og 和 JSON-LD @types 相等;網站 URL 位於網站的來源上。除非 PUBLIC_INDEXABLE=true,否則 robots 必須攜帶暫存用的 noindex, nofollow,而其餘部分在這些兩個標籤之後與即時網站相同;正式環境建置則完全比較 robots | |
sitemaps | dist/ 中的每個頂層 Sitemap(Astro 的 sitemap-index.xml,或 baseline/sitemaps.json 中的即時名稱,例如 wp-sitemap.xml 或 sitemap_index.xml)列出來源上完全相同的可索引 200 頁面(百分比跳脫比較不區分大小寫:%c3 等同於 %C3);robots.txt 中的每個 Sitemap: 行位於來源上,並命名 dist/ 中的一個檔案,或者該行是即時 robots.txt(capture/robots.txt)逐字攜帶的;每個 Feed 都建置了與即時相同數量的項目 | |
text | 每個頁面的可見文字等於 baseline/text/(字詞差異比較,5 個字詞的上下文) |
使用 --exclude 進行的捕獲會將其路徑前綴記錄在 baseline/meta.json 的 excluded 中:links 關卡不檢查位於其下的連結(就像連結到其他主機一樣,註記為「未檢查排除路徑下的 N 個連結」),而 sitemaps 關卡在即時 robots.txt 逐字攜帶該行時,接受位於其下的 robots.txt Sitemap: 行(同一主機上的多站點子站點的 Sitemap)。
4. 差異與決策
當建置無法或不應與即時網站匹配時,請閱讀 references/exceptions.md。簡而言之:一個條目是
{ path, field, from, to, reason, approvedBy, date };from: "" 記錄建置新增的值,
to: "" 記錄建置移除的值;不再匹配的例外,或命名關卡不檢查的路徑,會使關卡失敗。即時網站上的每個異常(缺少 H1、失效的標籤、
損壞的連結)預設都會被重現,並會獲得一個決策行(id、topic、choice、default、
reason、cost if wrong)來說明這一點以及修復方法。
一個記錄具有 kind 而非 field:{ "kind": "redirect", "path": "<source>", "reason": "...", "action": "none", "approvedBy": "...", "date": "..." } 表示「此路徑沒有重新導向,是有意為之」(為損壞的 href 記錄的即時重新導向,不應重新導向任何內容)。每個鍵都是必需的:它移除了即時行為,因此像任何例外一樣需要簽核。重新導向關卡會請求該路徑,並且僅在規則未重新導向它時才通過;在關卡不檢查的路徑上,它會失敗,顯示為 path not checked;verify-live 會跳過它並附上原因。所有其他關卡都會忽略它。
5. 暫存、提案與正式環境建置
PUBLIC_INDEXABLE 和 PROPOSALS 會改變 Astro 的建置內容,因此請在建置上設定它們,而不僅僅是在關卡執行時;在沒有它們的情況下進行的建置上執行關卡,會檢查錯誤的網站。僅在建置本身是為該主機製作時才傳遞 --origin;範本化建置的 site 是即時來源,因此其 Canonical 和 Sitemap 會保留在該來源上。
npm run build && npm run verify -- --endpoint ... --thank-you ... # 暫存
PROPOSALS=on npm run build && npm run verify:proposals -- --endpoint ... --thank-you ... # 提案
PUBLIC_INDEXABLE=true npm run build && npm run verify:release -- --endpoint ... --thank-you ... # 正式環境
暫存部署後,透過 HTTP 根據基準線檢查它。正式環境部署的檢查方式相同,但不需要 --expect-noindex:
npm run verify:live -- --base https://my-site.vercel.app --expect-noindex
node <skill-dir>/scripts/verify-live.mjs --base https://my-site.vercel.app --expect-noindex # 備用
--bypass "$VERCEL_AUTOMATION_BYPASS_SECRET" 僅在暫存被刻意保護時適用(保密值來自環境,絕不在聊天中貼上);公開的暫存別名則不需要。
它會檢查每個基準線路由是否為 200(或 --sample)、每個重新導向是否在一次跳轉中且具有相同類型的狀態碼、頁面是否具有 noindex(當且僅當設定了 --expect-noindex)、Sitemap,以及 20 個媒體檔案的位元組(或 --media-manifest,其 url 可以是 store 的絕對 URL 或在 <base><path> 檢查的網站路徑)。redirects-skipped.json 中列出的重新導向或被「無重新導向」例外(--skipped、--exceptions;兩者預設都從網站根目錄讀取)涵蓋的重新導向會被跳過,並以其原因命名。每個請求在 30 秒後超時,這是該檢查的失敗而不是封鎖。退出碼 3 表示它被封鎖:請等待並重新執行。退出碼 3 並帶有 deployment protection: 表示 Vercel 以其登入重新導向(302 到 vercel.com/sso-api)回應:請使用專案的正式環境別名(預設為公開)而非原始部署 URL;僅當暫存被刻意保護時才需要保密。”