閱讀約 12 分鐘 專案紀錄, 回憶錄, 個人網站, Cloudflare, Backblaze, OAuth

這幾天我到底做了什麼:48 小時建站全程操作回憶錄

不是教學文。是把我這兩天跟 AI 助手協作建站的每一步——點過的按鈕、貼過的指令、撞過的牆、罵過的話——按時間順序倒帶一遍,讓未來的我記得自己究竟做了什麼。

這篇是寫給未來的我看的

同系列前兩篇分別是:

這篇是流水帳——按時間順序記錄我這兩天到底做了什麼。沒有抽象的工程哲學,只有「那時候我看到 X 畫面 → 我貼了 Y 指令 → 然後撞到 Z 問題」的逐步回憶。

因為前兩篇寫完我自己讀都覺得「好像哪裡學到東西了又好像沒有」——抽象版本太遠,教學版本是寫給陌生人看的。我需要的是自己的操作回憶錄


Day 0|起心動念

最早的對話其實不是講網站,是在處理 Windows 螢幕方向熱鍵(Ctrl+Alt+R 翻轉螢幕)跟一個 PowerShell cc 別名映射到 claude 指令。聊到一半我突然丟出一句:

「我要的是個人網站兼資料儲存庫的概念喔!」

這句話徹底岔題,但也定錨了接下來兩天的所有工作。一個域名同時做兩件事:

  • 對外 → 履歷 / 作品集 / 部落格
  • 對內 → 我登入後才能存草稿、敏感檔案、寫文章

當下還沒做任何架構決策。只是一個需求被講出口。


Day 1 早|開工:claude_sandbox 那個資料夾

第一個明確的操作指令是我給 Claude 的:

「桌面上是不是有一個 claude_sandbox 資料夾,如果有的話幫我在該資料夾建立此專案後以全球 50 年網頁假設工程團隊及專業美編頁面設計工程團隊確保高品質美觀的專業網站呈現」

於是 Claude 在 C:\Users\USER\Desktop\claude-sandbox\personal-site\ 開了個新專案,第一輪做了:

  • npm create astro@latest 起手 minimal template
  • 加 Tailwind v4
  • src/site.config.ts(站台 SSoT)
  • src/content/{posts,works,photos,resume}/ 四個 collection
  • 寫 Header / Footer / Hero 元件、首頁、portfolio 列表

跑出來大概是個能看的英文版骨架。然後我下了第二個指令:

「頁面主要是給台灣人看的幫我翻成繁忠」

(沒錯,「繁中」打成「繁忠」。Claude 沒糾正我,照做翻成繁體中文。)

這時還只是純前端,沒任何後端、沒部署、沒登入。


Day 1 中|兩輪審計:四位專家 + 五位專家

我下一句說:

「你先自行出動專家團隊分析該專案弱點分析以便功能完善」 「做最完善的處理」

Claude 派了 4 位 AI sub-agent 平行 review(security / frontend design / devops / code review),找出 15 件 P0/P1。

然後我嫌不夠,下了:

/agent-teams-builder(後面接一段超長 prompt)

C 版本(進階版,有四層品質閘門)。這次跑的是真正的 Agent Teams(5 位獨立 Claude session,互相 mailbox 溝通),又找出 7 件問題。

兩輪修補總計 22 件。這時候我什麼按鈕都還沒按過——所有事情都是 Claude 在我電腦的 claude-sandbox 裡跑,我只在聊天框輸入指令。


Day 1 晚|第一次部署:撞 R2 bucket 不存在

我下了:

「幫我部屬到可以隨處開啟該網站,然後要有管理員機制讓我登入並編輯管理頁面資訊以及個人小雲端儲存空間」

開始部署流程。我這時候才第一次做真實的點擊操作:

  1. 開瀏覽器到 dash.cloudflare.com,註冊帳號
  2. Workers & PagesCreatePagesConnect to Git
  3. 授權 Cloudflare 讀我的 GitHub
  4. personal-site repo,框架選 Astro,按 Save and Deploy

第一次 deploy 失敗。錯誤訊息是 wrangler.toml 裡 reference 了一個 R2 bucket vault-files 但那個 bucket 不存在。

Claude 叫我註解掉 wrangler.toml 裡那段 [[r2_buckets]]

# [[r2_buckets]]
# binding = "VAULT_FILES"
# bucket_name = "vault-files"

push 後第二次 deploy 過了。網站上線在 https://personal-site-857.pages.dev

我接著問:

「所以現在已經部屬在網路上了?」

Claude 說對。我可以從手機開那個網址驗證。這是第一個 reality check 時刻——前面兩天 Claude 寫的東西終於在我電腦以外的地方跑起來了。


Day 1 深夜|「我不想綁卡ㄟ」

雲端儲存的部分——原計畫是 Cloudflare R2。我去 dashboard 開 R2 時跳出來「需要綁定信用卡」的頁面。我傳給 Claude:

「我不想綁卡ㄟ,萬一被扣款不就糟了」

接著問:

「你幫我確任 free 方案綁卡是否真的不會被收取費用跟額外收費?」

Claude 解釋了 R2 的計費邏輯給我聽,但結論是「綁卡這事本身就是風險來源」——萬一未來條款改、或 free tier 縮水、或我忘記取消,都會被扣。

於是我們整個雲端後端砍掉重來

  • Cloudflare R2 → Backblaze B2(永久免費 10 GB、不要綁卡)
  • 程式碼:functions/api/cloud/[[path]].ts 從 R2Bucket binding 重寫成 aws4fetch SigV4 打 B2 endpoint

我這時的操作:

  1. https://www.backblaze.com 註冊(沒要信用卡)
  2. BucketsCreate a Bucket → 取名 yian524-vault-files、Private、勾預設加密
  3. 記下 Endpoint:https://s3.us-east-005.backblazeb2.com
  4. App KeysAdd a New Application Key → 限定只能存取那個 bucket、Read+Write
  5. 這頁顯示 keyID 跟 applicationKey 我馬上複製到密碼管理器
  6. 回 Cloudflare Pages → Settings → Environment variables → 新增 5 條:
    B2_ENDPOINT, B2_BUCKET, B2_REGION, B2_KEY_ID, B2_APPLICATION_KEY
    B2_APPLICATION_KEY 那條我有勾 Encrypt
  7. Deployments → 最新一次 → Retry deployment

部署完成。第二次 reality check——/api/cloud/list 用 curl 打過去開始回真實的 B2 物件清單。


Day 2 早|Cloudflare Access 之死

下一步是登入機制。原計畫:用 Cloudflare Access 在 edge 層攔截 /vault/admin/api/cloud,要求 Email OTP,再加 Pages Function 內的 JWT 二次驗證。

我的操作:

  1. Cloudflare → Zero TrustAccessApplicationsAdd an application
  2. Self-hosted → 填 domain → 設 policy 「allow if email == edison31512@gmail.com
  3. 測試 → 開無痕視窗訪問 /vault → 跳到 https://edison31512.cloudflareaccess.com/... 的登入頁
  4. 輸入 email → 按「Send me a code」
  5. email 沒收到。

等了 10 分鐘還沒收到。我重發。還是沒收到

我去 Gmail 翻 Spam。沒有。

跟 Claude 抱怨後它建議我去設定 Login methods 改用其他驗證方式。它給了一個網址要我去點。我點開:

「怎麼做??你給的網址自己都不用確認能不能正確顯示嗎…你的操作指令根本找不到浪費我時間」

(這句是我打字爆氣的原話,截圖留著。)

問題在於 Cloudflare Zero Trust 改版了——文件裡的「Login methods」設定路徑跟實際 dashboard 對不上,Claude 給的 URL 進去看到的不是它預期的那個畫面。

我們三個小時內試了:

  • 重發 OTP(沒收到)
  • 改用其他 Identity Provider(找不到設定頁)
  • 砍掉 Access application 重建(一樣的問題)
  • 連 Email 信箱都改了(一樣沒收到)

最後 Claude 給我三個選擇:A、B、C。我打了:

C

C 是「砍掉 Cloudflare Access,全部自架」。


Day 2 中|砍掉重練:自架 GitHub OAuth

選 C 之後 Claude 寫了 6 個檔案:

functions/lib/auth.ts                 ← HS256 JWT sign/verify、cookie helpers
functions/_middleware.ts              ← 路由守門
functions/api/auth/login.ts           ← 起 OAuth 流程
functions/api/auth/callback.ts        ← 收 GitHub code、簽 JWT、設 cookie
functions/api/auth/logout.ts          ← 清 cookie
functions/api/auth/whoami.ts          ← debug 用,回 {authenticated, login}

我的操作:

  1. github.com/settings/developersOAuth AppsNew OAuth App
  2. 填 Homepage 跟 Callback URL(兩個都是 https://personal-site-857.pages.dev/...
  3. Register → 拿到 Client ID
  4. Generate a new client secret馬上複製(這頁刷新後就再也看不到)
  5. 回 Cloudflare Pages → Environment variables → 新增 4 條:
    VAULT_OAUTH_CLIENT_ID
    VAULT_OAUTH_CLIENT_SECRET    ← Encrypt
    VAULT_JWT_SECRET             ← 用 openssl rand -hex 32 產的
    VAULT_ALLOWED_LOGINS         ← yian524
  6. 砍掉 Cloudflare Access application(Zero Trust → Applications → ⋯ → Delete)
  7. push 一次 force redeploy

我以為這樣就好了。打開無痕視窗測 /vault

ERR_TOO_MANY_REDIRECTS

第三個坑來了。


Day 2 下午|Redirect Loop 之夜

我把畫面截圖給 Claude。Claude 第一直覺:「middleware 把 cookie 弄丟了」。

它叫我去 /api/auth/whoami

{"authenticated": true, "login": "yian524"}

cookie 明明設好了。但 /vault 還是 loop。

接下來 Claude 修了 middleware 的 next(modifiedRequest) 那段(從 next(upstream) 改成 return next();)。push、redeploy、重測。

還是 loop

Claude 接著做了一件事——叫我用 curl --max-redirs 20 -L 跟一個完全跟 vault 無關的頁面

curl -L --max-redirs 20 https://personal-site-857.pages.dev/portfolio

這條跟 20 次都還在 redirect。就是兇手:問題不只在 /vault整站都在 loop

排查到最後找到犯人:public/_redirects 裡有條規則:

/portfolio/    /portfolio    301

我寫這條是想把斜線去掉。但 Cloudflare Pages 對目錄型靜態檔會自動 308 加上斜線。所以:

/portfolio  →  308 加斜線  →  /portfolio/
/portfolio/ →  我的 301 去斜線 → /portfolio
/portfolio  →  308 加斜線  →  /portfolio/
... 永遠

我這幾天都沒發現是因為平常都點 nav link 進去(已經是有斜線版本),沒人會在網址列手打沒斜線

修法簡單到讓我懷疑人生:整個 _redirects 檔砍掉

rm public/_redirects
git commit -am "fix: drop _redirects, trust Cloudflare auto-trailing-slash"
git push

8 行的 commit 救活整個網站。


Day 2 傍晚|CSP 跟 Sveltia 的二連坑

/vault 通了,輪到 /admin(Sveltia CMS 後台)。打開後空白頁

開 DevTools Console 看到:

Refused to load the script 'https://unpkg.com/@sveltia/cms@0.96.0/...'
because it violates the following Content Security Policy directive:
"script-src 'self' 'unsafe-inline'"

但我明明_headers 裡寫了 /admin/* 允許 unpkg。Claude 跟我用 curl 看實際的 header:

curl -I https://personal-site-857.pages.dev/admin/
# Content-Security-Policy: ...沒允許 unpkg...
# Content-Security-Policy: ...允許 unpkg...

兩條 CSP 同時出現。瀏覽器收到多條會強制執行交集——/* 那條沒允許 → 永遠擋。

修法:把 CSP 整合成一條全域,把 unpkg 加進 allow-list。

但修完還是空白頁。第二個小坑:我用的 Sveltia 是 0.96.0,已經 60+ 版前的東西。bump 到 0.158.1,重新算 SRI hash:

curl -s https://unpkg.com/@sveltia/cms@0.158.1/dist/sveltia-cms.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

把新 hash 貼進 public/admin/index.htmlintegrity= 欄位。push、redeploy。

第三個小坑:Sveltia 起來了但載入閘門永遠不消失——我寫的 fade out 邏輯依賴 [data-svelte] selector,新版已不發那個 attribute。改成 2.5 秒後 unconditional fade。

第四個小坑:Google Fonts 也被擋。再把 fonts.googleapis.comfonts.gstatic.com 加進 CSP。

/admin 終於可以登入寫文章。


Day 2 晚|「幫我設計這個 logo 連續點 6 下即可跳到管理後台」

Sveltia 通了之後我問:「/admin 連結要不要放 nav?」自答:不要,給訪客看沒價值,徒增攻擊面。

但每次寫文章都要打網址也煩,我下了:

「幫我設計這個 logo 連續點 6 下即可跳到管理後台」

Claude 在 Header.astro 結尾加 27 行 vanilla JS。實際測試:

  • 一般訪客點 logo → 等 250ms → 回首頁(無感)
  • 我連點 6 下(1.5 秒內)→ 跳 /admin

整個專案我最得意的細節——零視覺暗示,但永遠能進後台。


3 次 secret 外洩事件簿

這幾天我三次把 secret 貼進跟 Claude 的對話框:

  1. 第一次:B2 master key(Account-level、權限最大)
  2. 第二次:B2 第一把 scoped key(限定 bucket)
  3. 第三次:GitHub OAuth Client Secret

每次發生時 Claude 都立刻打斷我,要我跑「撤銷舊 → 產新 → 設環境變數 → 確認新的能用 → 永遠不再說出舊的」流程:

# B2: 到 dashboard App Keys → ⋯ → Delete
# 然後 Add a New Application Key → 馬上複製

# Cloudflare Pages: Settings → Environment variables → 編輯 B2_KEY_ID / B2_APPLICATION_KEY
# 然後 Deployments → 最新 → Retry deployment

# GitHub OAuth: Settings → Developers → 那個 App → Generate a new client secret
# 立刻去 Cloudflare Pages 改 VAULT_OAUTH_CLIENT_SECRET → Retry deployment

每次大概 3-5 分鐘搞定。

教訓我自己貼在便利貼上:secret 永遠只貼進三個地方,不貼任何聊天框——

  1. PowerShell 的 wrangler secret put 互動式 prompt
  2. Cloudflare Pages dashboard 的 Encrypt env var 欄位
  3. 密碼管理器

我這兩天按過的關鍵按鈕清單

按時間順序整理(給未來重做時對照):

Cloudflare

  • dash.cloudflare.com 註冊
  • Workers & Pages → Create → Pages → Connect to Git → 選 repo
  • Build settings: Astro / npm run build / dist
  • Settings → Environment variables → 加 5 條 B2 + 4 條 VAULT
  • Zero Trust → Access → Applications → 建一條(後來砍)
  • Zero Trust → Access → Applications → ⋯ → Delete(換自架後)

Backblaze

  • backblaze.com 註冊(不要信用卡)
  • Buckets → Create a Bucket → Private + 預設加密
  • App Keys → Add a New Application Key → 限 bucket + Read/Write
  • Account → Caps & Alerts → 設物理上限 0.99 GB/day(防爆)

GitHub

  • Settings → Developers → OAuth Apps → New OAuth App(設 Homepage + Callback URL)
  • Generate a new client secret(兩次:被外洩過一次後重產)
  • Settings → Security → Personal access tokens(Sveltia CMS 用,後改 OAuth Worker)

本機

  • PowerShell 跑 npx wrangler login(開瀏覽器授權一次)
  • PowerShell 跑 npx wrangler deploy(部署 Sveltia OAuth Worker)
  • PowerShell 跑 npx wrangler secret put GITHUB_CLIENT_ID/SECRET/ALLOWED_DOMAINS

Git / GitHub repo

  • gh repo create yian524/personal-site --public --source=. --push
  • 約 28 個 commit 一路 push 到 main,每個觸發 Cloudflare auto-deploy

我這兩天下過的關鍵指令清單

# 起手式
npm create astro@latest personal-site -- --template minimal
cd personal-site && npm install -D tailwindcss @tailwindcss/vite

# B2 接 aws4fetch
npm install aws4fetch

# Sveltia OAuth Worker
cd workers/sveltia-cms-auth
npm install
npx wrangler login
npx wrangler deploy
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put ALLOWED_DOMAINS

# JWT secret 產生(一次性)
openssl rand -hex 32

# Sveltia SRI hash 計算(每次 bump 版本要重算)
curl -s https://unpkg.com/@sveltia/cms@0.158.1/dist/sveltia-cms.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

# 解 redirect loop debug
curl -L --max-redirs 20 https://personal-site-857.pages.dev/portfolio

# 解 CSP debug
curl -I https://personal-site-857.pages.dev/admin/

# 緊急 revert(Astro 6 那次)
git revert 35d3734 && git push

# 砍 _redirects(救全站 redirect loop)
rm public/_redirects
git commit -am "fix: drop _redirects" && git push

如果再來一次,這幾步可以跳過

  • 不要試 Cloudflare R2——直接去 Backblaze B2,省 6 小時
  • 不要試 Cloudflare Access Email OTP——直接自架 GitHub OAuth,省 3 小時
  • 不要寫 public/_redirects——信任 Cloudflare 預設斜線行為
  • 不要試 _headers per-path CSP override——直接寫一條全域 CSP 含所有 allow-list
  • 不要把 secret 貼進聊天框——三次教訓夠了
  • 不要合 Astro / Tailwind 主版本 dependabot PR——加 ignore rule,省一次 deploy 災難

如果跳過上面所有彎路,這個網站從零到上線實際只需要 8-12 小時。我花了 48 小時是因為走了完整路線——但也因此每一個決策的「為什麼不選 X」我都有實戰證據。


結語

寫這篇花了我兩個小時,但值得。前兩篇我寫完都覺得有點失落——一個太抽象,一個太教科書。我需要的是這篇——「我那時候在哪裡按了什麼」的記錄。

兩天後、兩個月後、兩年後我重做這個網站時,會先打開這篇。看到「Day 2 下午|Redirect Loop 之夜」那一段時,會記得「喔對 _redirects 不要寫」。看到「3 次 secret 外洩事件簿」時,會記得「先把便利貼貼好」。

這就是這篇文章存在的意義——不是給陌生人讀的教學,是給我自己對的時間提示自己「對對對是這樣」的一份記事

如果你也是工程師,建議你完成大專案後寫一份這種回憶錄。不要寫教學文(那是給別人的),寫自己當下的操作流水帳。半年後你會感謝那兩個小時。


寫於 2026-05-02 深夜,網站上線後第二天。完整 source code:github.com/yian524/personal-site