主題: Cloudflare

綠色 Build 不等於已上線:Astro 靜態站的發佈驗證紀錄

從這個 Astro 部落格出發,拆開本機檢查能證明什麼、不能證明什麼,以及 static assets 如何進到 Cloudflare Worker。

看到 pnpm build 綠燈後,就把一篇技術文章叫做「已發布」,很誘人。這個感覺也不完全錯:頁面 render 了、MDX parse 了,dist 也產生了。

但證據還差一段。

這篇記錄本站採用的發佈邊界。它不是 benchmark,也不會因為本機指令成功就聲稱 production 已更新。這個差別平常聽起來像吹毛求疵,直到 stale deployment、錯 branch 或 asset path 出錯,讓本機完全正確的頁面根本沒被讀者看見。

發佈形狀與歷史範圍

這個 repo 的 release path 很窄:

{
  "deploy": "pnpm build && wrangler deploy"
}

Wrangler 設定把 static assets 指向 build output:

{
  "assets": {
    "directory": "./dist"
  }
}

Cloudflare 會把設定的 assets directory 當成 Worker deployment 的一部分上傳,再將檔案送給讀者。因此 dist 是必要 artifact,卻仍只是本機檔案,直到 Wrangler 成功部署為止。正確的 static-site workflow 必須拆成兩個問題:build 有沒有產出正確檔案?與live domain 是否正在送出這批檔案?

2026-08-10 的驗證紀錄

在 2026-08-10 的內容更新中,我先跑三個檢查才把 source 視為 ready:

pnpm lint
pnpm build
pnpm seo:check

每個指令回答的問題不同:

檢查 能提供的證據 不能提供的證據
pnpm lint 編輯到的 Astro、TypeScript 與 MDX 相鄰 source 通過既有 lint 規則。 頁面已 build 或已 deploy。
pnpm build Astro 可 render routes 並產生 dist。 production Worker 已收到這份 dist。
pnpm seo:check 已建好的 sitemap、canonical URL、hreflang、JSON-LD 與 route assertion 符合本站規則。 搜尋引擎已 crawl 或 rank 這項更新。

這個拆分修正了我自己的 release 用語:local valid 與 live 不是同義詞。本機 build 後正確的交接是 ready to deploy,不是 deployed。

先看 build 出來的證據,再打開 dashboard

第一輪 inspection 不需要 browser automation:

test -f dist/sitemap.xml
test -f dist/robots.txt
rg 'static-astro-release-evidence' dist/en dist/zh-TW

前兩個指令確認 crawler entry points 已生成;最後一個確認新 field note 的雙語版本都進入 static output。這是一個便宜卻重要的檢查:MDX 檔名或 locale suffix 打錯時,很容易誤以為是內容問題,其實 route 根本不存在。

以下是當時使用舊網域的歷史指令;現行網域檢查見後面的更新段落:

curl -fsSI https://tech-blog.hungchihsueh.com/en/
curl -fsS https://tech-blog.hungchihsueh.com/sitemap.xml | xmllint --noout -

第一個指令確認已發布的 route 有回應;第二個驗證 live sitemap XML。兩者都不能保證每位讀者都已穿透每一層 cache 看見新版本,但證據強度遠高於只看本機 dist。

2026-09-10 更新:如何驗證這一版

現行公開網域是 hch-log.com,Worker 名稱仍是 tech-blog。src/worker.ts 將舊網域與 www 導向新網域,文章仍由靜態資產提供。舊網域回應 301 是轉址證據,不能當成新文章已更新。

在專案根目錄執行以下檢查。pnpm seo:check 已包含建置,不必在它前面再 build 一次;最後的 assertion 驗證兩個實際文章檔案,而非只搜尋任意頁面的相關文章連結。

git status --short --branch
pnpm test
pnpm lint
pnpm seo:check
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const slug = 'field-notes/static-astro-release-evidence';
for (const locale of ['zh-TW', 'en']) {
  const html = readFileSync(`dist/${locale}/article/${slug}/index.html`, 'utf8');
  assert(html.includes(`https://hch-log.com/${locale}/article/${slug}/`));
  assert(html.includes('2026-09-10'));
  assert(html.includes('hch-log.com'));
}
console.log('Both local article pages contain the current revision marker.');
JS

2026-09-10 是本次修訂的標記;日後發布不同修訂,要把 assertion 換成那次特有的內容。這些指令只檢查本機,不會上傳檔案。

正式部署之後,再針對本文兩個 locale 的 GET 回應核對:HTTP 200、canonical 是 hch-log.com、修訂內容存在,以及回應中的 CSS/JS 資源能正常取得。不要只看首頁或 HEAD 成功。正式 sitemap 也必須包含本文與正確的更新日期。

觀察 下一步
dist 沒有修訂內容 檢查分支、MDX 路徑與 build,不要先清 CDN
dist 正確,正式文章仍舊 核對部署目標、版本與 HTTP 快取
HTML 已新,資源 404 核對資源上傳與舊資源保留;不要用重新整理掩蓋

本次文章補強的驗證範圍是本機建置與預覽;未因此宣稱正式站已發布新版。搜尋收錄與 AdSense 審查還是另外的結果。

取捨與失敗模式

這個 static setup 刻意讓 runtime 很小:MDX 在 build 時變成 HTML,Worker 只送 assets。一般文章閱讀因此沒有 database call 或 server rendering。代價是內容變更一定要 build 與 deployment;不存在一個隱藏 CMS write 可以讓 production 自己變新。

有三種 failure mode 值得直接寫出來,因為它們很容易導向信心滿滿卻錯誤的狀態回報:

  1. 在錯的 branch build。 artifact 合法,卻沒有目標文章。
  2. build 成功但沒有 deployment。 本機的 dist 更新了,live Worker 仍送出舊 assets。
  3. deployment 漏掉需要的生成檔。 route 雖然能開,robots.txt、sitemap output 或某個 locale page 可能不見了。

對策同樣很無聊:發布前檢查 branch 與 worktree,跑 build checks,只在獲得授權後 deploy,再測試精確的 live URLs。我寧可保留這條順序,也不想為了它再建一層 release abstraction;這個站已經有能證明每個邊界的指令。

我刻意沒有加入什麼

我沒有為此加入 CMS、release dashboard 或 Container。本站已有 static build output 與 Worker assets directory;增加基礎設施只會多一個要驗證的狀態,卻不會讓證據邊界更清楚。

若未來真的需要排程動態內容或個人化頁面,那是新的需求,值得重新設計。現在則是小清單加上直接 URL,更容易信任,也更容易除錯。

資料來源