閱讀約 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) 未來重建時的我自己。
起點:兩個衝突的需求
我同時想要兩件事:
- 公開的個人品牌站:放作品集、履歷、文章,希望被 Google 索引、被人資看到
- 私人雲端:放未公開草稿、私人照片、敏感檔案,必須登入加雙重驗證才能看
主流方案各有缺陷:
- Notion / Carrd:公私區分不夠細,私密頁要付費才能加密碼
- WordPress:外觀俗、SEO 要調、要不斷更新外掛防駭
- Nextcloud / FileBrowser:是「網路硬碟」不是「網站」,當履歷頁不堪用
- GitHub Pages:私人 repo 沒辦法給單一頁面加 2FA
最後選擇 Astro + Cloudflare 全託管棧——一個域名兩個分區、每篇文章用 frontmatter 一個 boolean 欄位切換公私、零維運成本。
架構決策鏈
最終 stack 與選擇理由:
| 元件 | 選擇 | 為什麼 |
|---|---|---|
| 前端框架 | Astro 5 | 純靜態 SSG、Markdown 內建、可混 React 元件、SEO 滿分 |
| 樣式 | Tailwind v4 + 自訂 OKLCH tokens | CSS-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 免費 |
| 後台 CMS | Sveltia CMS | git-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 / P2 | 12 件(已排程修補) |
| 我自己手寫 commit 數 | 0(全 AI 協作) |
工程哲學的兩個體會
第一:fail-closed 不是技術,是文化。每寫一行涉及驗證的程式碼,都要問「設定缺失或驗證失敗時會發生什麼?」如果答案不是「拒絕請求」,就是錯的。
第二:兩層防禦比單層強三倍。Cloudflare Access + JWT 驗證看似重複,但真正壞掉時你會感謝過去那個多寫了 50 行 fail-closed 程式碼的自己。
整個過程的 git history 公開在 github.com/yian524/personal-site,commit message 寫得相當白話,可當作學習材料。