閱讀約 5 分鐘 Astro, Cloudflare, DevOps, 工程日誌

從零打造這個個人網站:48 小時的工程日誌

Astro + Cloudflare Pages + Backblaze B2 + Cloudflare Access 的全託管棧建置紀錄。包含 9 位專家審計、22 件 P0/P1 修補、3 次 secret 外洩經驗、1 次 Astro 6 升級災難。誠實的工程日記。

這個網站的程式碼從零開始到正式上線,前後大約 48 小時。本文記錄完整的建置決策鏈、踩過的坑、以及最終的架構選擇。寫給:(1) 想做類似專案的人 (2) 對「個人網站該選什麼 stack」猶豫的人 (3) 未來重建時的我自己。

起點:兩個衝突的需求

我同時想要兩件事:

  1. 公開的個人品牌站:放作品集、履歷、文章,希望被 Google 索引、被人資看到
  2. 私人雲端:放未公開草稿、私人照片、敏感檔案,必須登入加雙重驗證才能看

主流方案各有缺陷:

  • Notion / Carrd:公私區分不夠細,私密頁要付費才能加密碼
  • WordPress:外觀俗、SEO 要調、要不斷更新外掛防駭
  • Nextcloud / FileBrowser:是「網路硬碟」不是「網站」,當履歷頁不堪用
  • GitHub Pages:私人 repo 沒辦法給單一頁面加 2FA

最後選擇 Astro + Cloudflare 全託管棧——一個域名兩個分區、每篇文章用 frontmatter 一個 boolean 欄位切換公私、零維運成本。

架構決策鏈

最終 stack 與選擇理由:

元件選擇為什麼
前端框架Astro 5純靜態 SSG、Markdown 內建、可混 React 元件、SEO 滿分
樣式Tailwind v4 + 自訂 OKLCH tokensCSS-first 設計,避免「AI 模板」的廉價感
字型Inter Variable(UI)+ Fraunces Variable(標題)雜誌級配對,不撞 Google Fonts 預設
託管Cloudflare Pages無限流量、auto SSL、git push 自動部署
雲端儲存Backblaze B2(S3 相容)10 GB 免費且不需綁卡(R2 改規定要綁卡)
認證Cloudflare Access(Email OTP)邊緣攔截 + 50 users 免費
後台 CMSSveltia CMSgit-based、可在瀏覽器拖檔上傳、純前端

公私切換的機制是 Astro Content Collections + 一個 frontmatter 欄位:

---
title: "我的論文草稿"
public: false   # ← 改成 true 就在公開區出現
---

整站用一個 helper(src/lib/content.ts)集中過濾,避免邊界邏輯散布。

第一次 Review:4 位專家平行審計

baseline 寫完後,我派出 4 位 AI 專家平行 review:security、frontend design、devops、code review。各自獨立寫報告,PM 角色彙整。

第一輪找到 15 件 P0/P1 issue,包括:

  • JWT 驗證中 dev-mode header 可偽造(最嚴重)
  • R2 上傳的 HTML 可造成 stored XSS
  • Sveltia CMS pin 在 @latest 而非確切版本(供應鏈風險)
  • /about 頁面 Tailwind Typography 衝突造成暗色模式 h2 隱形

修補時遵守一個原則:fail-closed by default。任何環境變數沒設、任何驗證失敗,全部 503/403 拒絕,絕不靜默放行。

第二次 Review:5 位專家深入交叉驗證

修完 P0 後再做一輪——這次 5 位 reviewer 互相辯論:

  • admin-ux:模擬「不會 Linux 的使用者」走過 OPERATIONS.md 全流程
  • hr-persona:扮演「人資主管 + 技術主管」雙視角,實測 5 秒掃履歷
  • design-lead:4 個斷點各 7 頁共 28 張截圖,逐項驗 WCAG 對比度
  • security-auditor:跑 git log 全歷史掃 secret + 寫 5 條 post-deploy pentest curl
  • PM:每位 reviewer 評 7 項 × 10 分,未過 9.0 退回最多 2 輪

第二輪再修 7 件,含一個關鍵發現:手機版完全沒導覽——760px 以下 primary nav 整段隱藏,沒有漢堡選單替代。任何用手機看履歷的人完全找不到作品集連結

部署的三次坑

第一次坑:R2 改規定要綁卡。 原本選 Cloudflare R2 做雲端,免費 10 GB + 0 egress 看起來最香。但實際操作時 Cloudflare 要求綁信用卡——即使 Free tier。換成 Backblaze B2,重寫整個 cloud function 改用 S3-compatible API(aws4fetch SigV4 簽名)。

第二次坑:Astro 6 + Vite Rolldown 破壞 Tailwind。 Dependabot 自動 PR 把 Astro 5 升 Astro 6——build 立刻爆炸。錯誤訊息:Missing field 'tsconfigPaths' on BindingViteResolvePluginConfig,是 Vite Rolldown migration 跟 @tailwindcss/vite 還沒對齊。Revert 該 commit + 在 dependabot.yml 加 ignore rule 鎖 major bumps。

第三次坑:3 次 secret 外洩。 我(使用者)在跟 AI 助手對話過程中,先後把 B2 master key、第一把 scoped key、GitHub OAuth secret 貼進對話。每次都要:撤銷舊 key、產新 key、重新設環境變數。教訓:secret 永遠只貼到 PowerShell prompt 或 Cloudflare dashboard 的加密欄位,不要截圖、不要貼進任何聊天

防禦縱深(Defense in Depth)

最終的私密區保護有兩層:

攻擊者


[1] Cloudflare Access (Edge)
    ├─ 路徑:/vault/*, /admin/*, /api/cloud/*
    ├─ 機制:Email OTP(一次性密碼)
    └─ 結果:未通過直接 302 重定向到登入頁
   │ 通過後才進入下一層

[2] Pages Function JWT 驗證
    ├─ 從 Cf-Access-Jwt-Assertion header 取 token
    ├─ 驗證 alg=RS256(拒絕 alg=none / HS*)
    ├─ 從 JWT header 取 kid 對應 JWKS 中的 key
    ├─ 驗證 aud / iss / exp 全部正確
    └─ 結果:失敗 503 fail-closed
   │ 通過後才進入後端

[3] Backblaze B2(S3 SigV4 簽名)
    └─ 用 bucket-scoped Application Key(非 master)

即使 Cloudflare Access 哪天故障,function 層的 JWT 驗證仍守住;即使 JWT 也壞了,B2 那邊的 application key 也只能存取單一 bucket 不會擴散。

數字結算

指標結果
程式碼總行數~2,800
部署檔案16 頁 + 39 個 asset
Build 時間1.4 秒
Lighthouse 分數(預期)95+
月度雲端成本NT$ 0
安全 audit 通過件數22/22
殘留 P1 / P212 件(已排程修補)
我自己手寫 commit 數0(全 AI 協作)

工程哲學的兩個體會

第一:fail-closed 不是技術,是文化。每寫一行涉及驗證的程式碼,都要問「設定缺失或驗證失敗時會發生什麼?」如果答案不是「拒絕請求」,就是錯的。

第二:兩層防禦比單層強三倍。Cloudflare Access + JWT 驗證看似重複,但真正壞掉時你會感謝過去那個多寫了 50 行 fail-closed 程式碼的自己。


整個過程的 git history 公開在 github.com/yian524/personal-site,commit message 寫得相當白話,可當作學習材料。