🚀 靜態網站建置 SOP 手冊
基於
ming-travel-blog(travel.minglab.tw)和ming-website(minglab.tw)的真實開發經驗 搭配 OpenCode AI 協作,一人 + AI 即可完成
目錄
序章
技術架構 — 一句話解釋
你的網站 = 純 HTML 檔案,放在全世界最快的 CDN 上
Astro v6(把內容編譯成超輕量 HTML)
+ Tailwind CSS v4(自動生成最小化的樣式)
+ Markdown(寫文章跟打 Word 一樣簡單)
+ Sveltia CMS(瀏覽器開後台,填表單就更新網站)
+ Cloudflare CDN(全球 330+ 城市都有伺服器)
+ GitHub Actions(照片上傳自動壓縮 90%)
結果:零伺服器、零資料庫、零月費、零維護、永遠不會被駭
跟其他方案比
| 我們的方案 | WordPress | Wix / Squarespace | 找人客製 | |
|---|---|---|---|---|
| 建置費 | 一次性 | 一次性 | 無 | 高 |
| 月費 | $0 | $5-25 | $16-40 | $0 |
| 主機費 | $0 | $5-30/月 | 內含 | $5-30/月 |
| 載入速度 | < 1 秒 | 2-5 秒 | 2-8 秒 | 看品質 |
| SEO 評分 | 95+ | 70-90 | 60-80 | 看品質 |
| 安全性 | 無法被駭 | 需定期更新 | 平台負責 | 需維護 |
| 備份 | GitHub 永久 | 需外掛 | 平台負責 | 需自己來 |
| 搬家 | 一鍵搬走 | 可匯出 | 困難 | 困難 |
| 後台 | 表單式直覺 | 功能多但複雜 | 拖拉式 | 不一定 |
| 照片管理 | 自動壓縮 90% | 需外掛 | 自動 | 不一定 |
| 專業信箱 | ✅ 免費 | NT$200-300/月 | ❌ 自費加購 | NT$200-300/月 |
| 監控告警 | ✅ 免費 | 需外掛 | 平台內建 | 需自行 |
| 自動化驗證 | ✅ 一鍵 audit.sh | ❌ | ❌ | ❌ |
| 手機版 | 自動適應(含上傳壓縮) | 看主題 | ✅ | 看品質 |
| 原始碼 | 客戶擁有 | ✅ | ❌ | ✅ |
| 被平台綁架 | 完全沒有 | 有一點 | 嚴重 | 沒有 |
14 大優勢
| # | 優勢 | 對客戶的意義 |
|---|---|---|
| 1 | 永久零月費 | 網站做好後永遠不用再付錢,唯一成本是網域年費 |
| 2 | 全球極速 | Cloudflare CDN 330+ 城市有節點,載入 < 1 秒 |
| 3 | 無法被駭 | 無資料庫 = 無 SQL injection,無後端 = 無伺服器入侵 |
| 4 | SEO 天生高分 | 純 HTML 網站 Google 最愛,載入快 + 結構乾淨 |
| 5 | 客戶零門檻 | 瀏覽器 → 後台 → 填表單 → 儲存 → 自動更新 |
| 6 | 永遠不會壞 | 無伺服器、無套件更新、無版本不相容 |
| 7 | 原始碼是客戶的 | 放在客戶自己的 GitHub 帳號下 |
| 8 | 照片自動壓縮 | JPG 上傳 → 自動轉 WebP,檔案縮小 90% |
| 9 | 可以無限複製 | 同一個架構快速生出多個不同主題的網站 |
| 10 | 無平台綁架 | 搬家只需把 HTML 丟到任何主機 |
| 11 | 專業信箱內建 | info@你的網域,Cloudflare Email Routing 免費,不用買 Google Workspace |
| 12 | 網站掛了先知道 | UptimeRobot 每 5 分鐘監控,掛了寄 Email,比客戶早一步 |
| 13 | 交付前自動驗證 | audit.sh 一鍵 5 階段檢查(dead link + sitemap + localhost + GitHub Pages + 全頁面 200),其他公司沒在做 |
| 14 | 手機上傳零負擔 | 瀏覽器自動壓縮大照片(>1MB),客戶不用裝任何 App,選照片直接傳 |
7 個限制(以及解法)
| # | 限制 | 影響 | 解法 |
|---|---|---|---|
| 1 | 更新延遲 2-3 分鐘 | 儲存後不會秒更新 | CDN 全球同步正常時間 |
| 2 | 無法做會員登入 | 不適合帳號系統 | 第三方服務即可 |
| 3 | 無法做購物車 | 不適合電商 | Shopify 更適合電商 |
| 4 | 照片約 200-500 張 | 長期大量需管理 | 定期清理即可 |
| 5 | 無法即時留言 | 無內建留言板 | Disqus / Facebook Comments |
| 6 | 單人編輯 | 一個編輯者 | 共用 GitHub 帳號 |
| 7 | 部分英文介面 | Sveltia CMS 按鈕是英文 | 有完整中文使用說明 |
適合的客戶類型
| 客戶類型 | 適合度 | 說明 |
|---|---|---|
| 🧳 旅遊部落客 | ⭐⭐⭐⭐⭐ | 最完美方案,內建目的地管理、Lightbox |
| 📸 攝影師作品集 | ⭐⭐⭐⭐⭐ | Lightbox + 自動 WebP + 分類標籤 |
| 🏢 小型企業形象站 | ⭐⭐⭐⭐ | 關於我們、服務介紹、聯絡表單 |
| ✍️ 個人品牌 / KOL | ⭐⭐⭐⭐⭐ | 部落格 + 社群連結 + 零維護 |
| 📚 知識庫 / 文件站 | ⭐⭐⭐⭐ | Markdown 原生支援 |
| 🛒 電商 | ⭐ | 沒有購物車和金流 |
| 👥 社群平台 | ⭐ | 沒有會員系統 |
| 📊 SaaS 產品頁 | ⭐⭐ | 沒有後端 API |
跟傳統網站公司比
| 客戶會問 | 傳統網站公司 | 我們 |
|---|---|---|
| 「做好多少錢?」 | NT$ 50,000-150,000 | NT$ 25,000-50,000 |
| 「以後還要付什麼?」 | 主機費 NT$ 3,000-10,000/年 + 維護費 | $0 |
| 「我能不能自己更新?」 | 工程師時薪 NT$ 1,200-2,500/小時 | 自己開瀏覽器寫文章 |
| 「三年總成本?」 | NT$ 60,000-200,000+ | NT$ 25,000-50,000(一次性) |
| 「不滿意可以搬家嗎?」 | 困難 | 隨時,原始碼是你的 |
量化指標
| 指標 | 數據 | 評價 | 說明 |
|---|---|---|---|
| 原始碼大小 | ~4 MB | 🟢 | 不含 node_modules,極輕量 |
| GitHub Repo | ~24 MB | 🟢 | 佔免費 1GB 配額的 2.4% |
| 頁面數量 | 19 頁(含 RSS) | 🟢 | 含首頁、部落格、目的地等 |
| Build 時間 | < 1 秒 | 🟢 | Astro 靜態生成 |
| 部署時間 | 1-2 分鐘 | 🟡 | Cloudflare CDN 同步 |
| Lighthouse | 95+ | 🟢 | 靜態網站天生高分 |
| 圖片空間 | 4 張 / 1.9 MB | 🟢 | Actions 自動轉 WebP |
| 程式碼重複 | 0 | 🟢 | 無冗餘 |
| 閒置檔案 | 0 | 🟢 | 已清理乾淨 |
這是商業模版嗎?
是,而且是最理想的商業模版。 只需幾個步驟就可複製給新客戶:
| 要改的 | 時間 |
|---|---|
config.yml — 調整 collection 欄位 | 30 分 |
.astro 頁面 — 改配色、Logo 文字 | 20 分 |
astro.config.mjs — 改 site URL | 1 分 |
內容 .md — 清空,填客戶自己的 | 20 分 |
從零到上線:3-5 小時。
客戶溝通金句
「你的網站跟 Google、Facebook 跑在同一條高速公路上。」
「沒有伺服器 = 沒有月費 = 不會被駭。」
「三年後你只會付網域費,網站還是一樣快。」
「你寫文章,其他全部自動化。」
「這不是省錢,這是把錢花在對的地方。」
[[#table-of-contents|← 回目錄]]
第 1 章:準備工具與環境
👤 你:安裝軟體、註冊帳號 — 🤖 AI:確認環境就緒、產出費用總表
1.1 需要安裝的軟體
| # | 工具 | 用途 | 何時用 | 安裝 | 必裝 |
|---|---|---|---|---|---|
| 1 | Node.js >= 22 | 執行 Astro、npm run dev/build | 開發全程 | nodejs.org | ✅ |
| 2 | Git | 版本控管、push 觸發部署 | 開發全程 | brew install git | ✅ |
| 3 | Homebrew | macOS 套件管理(裝其他工具的前提) | 安裝工具時單次 | brew.sh | ✅ |
| 4 | OpenCode | AI 協作:寫碼、審計、除錯、文件產出 | 開發全程 | opencode CLI | ✅ |
| 5 | lychee | 全站 dead link 掃描(audit.sh 第 1 階段) | 交付前跑一次 | brew install lychee | ✅ |
| 6 | curl | HTTP 測試:檢查頁面回 200、sitemap 正確、RSS 正常 | 開發 + 交付 | macOS 內建 | ✅ |
| 7 | grep | 文字掃描:搜尋 localhost 殘留、舊部署網址、技術名洩漏 | 開發 + 交付 | macOS 內建 | ✅ |
| 8 | dig | DNS 查詢:檢查網域 A 記錄是否生效 | 部署除錯 | macOS 內建 | ✅ |
| 9 | ssh | GitHub 連線測試:ssh -T git@github.com | 部署前置 | macOS 內建 | ✅ |
| 10 | Chrome | Lighthouse 效能測試(§9.4) | 交付前 | google.com/chrome | — |
| 11 | UptimeRobot | 網站監控告警(掛了寄 Email 給你,§9.7) | 上線後永久 | uptimerobot.com | — |
| 12 | Obsidian | Markdown 閱讀器,支援 Wiki Link 跳轉、搜尋 | 讀手冊 | obsidian.md | — |
| 13 | VS Code | 程式碼編輯器(選配,grep/curl 為主) | 進階除錯 | code.visualstudio.com | — |
前 9 項是一條龍安裝:
brew install git node lychee。其他 macOS 內建。
1.2 需要註冊的服務
| 服務 | 用途 | 網址 |
|---|---|---|
| GitHub | 存放程式碼(Private repo) | github.com |
| Cloudflare | 部署 + DNS + CDN | dash.cloudflare.com |
| 網域 | 自訂網址 | Namecheap / GoDaddy / 中華電信 |
1.3 費用總表
| 項目 | 費用 |
|---|---|
| GitHub Private repo | $0 |
| Cloudflare Workers | $0(10 萬次請求/天) |
| Cloudflare Pages | $0 |
| Sveltia CMS | $0 |
| Astro | $0 |
| 網域 | ~$10-30/年 |
| 每月總計 | $0 |
| 每年總計 | ~$10-30 |
1.4 工具使用順序與完整流程
第一階段:環境準備(一次性,開工前)
① brew install git node lychee # 👤 你操作
② 註冊 GitHub → 開 Private repo # 👤 你操作
③ 註冊 Cloudflare → 等網域設定 # 👤 你操作
④ ssh-keygen + 貼到 GitHub SSH Keys # 👤 你操作
⑤ 買網域 → nameserver 指向 Cloudflare # 👤 你操作
第二階段:開發(每個專案)
| 步 | 誰 | 工具 | 指令 | 做什麼 |
|---|---|---|---|---|
| 1 | 👤 | — | 貼起手式提示詞 | 告訴 OpenCode 你的網站類型 + 風格 |
| 2 | 🤖 | OpenCode | — | AI 建 AGENTS.md + 視覺規範表,你確認 |
| 3 | 🤖 | OpenCode | — | AI 產出 .astro 頁面、config.yml、index.html |
| 4 | 👤 | npm | npm run dev | 本地預覽 http://localhost:4321,你驗收 |
| 5 | 👤 | npm | npm run build | 確認 0 error 才能 push |
| 6 | 🤖 | grep | grep -rn "關鍵字" src/pages/ | AI 跑技術名防洩漏檢查 |
第三階段:部署後檢查(交付前)
| 步 | 誰 | 工具 | 指令 | 做什麼 |
|---|---|---|---|---|
| 1 | 🤖 | audit.sh | ./audit.sh | AI 一鍵跑 5 階段檢查 |
| 2 | 🤖 | lychee | (由 audit.sh 自動執行) | AI 掃描全站 dead link |
| 3 | 👤 | curl | curl -sL https://網域/ | 你確認頁面標題、sitemap 正確 |
| 4 | 👤 | Chrome | DevTools → Lighthouse | 你確認效能/無障礙/SEO 分數 |
| 5 | 👤 | 瀏覽器 | 點每一頁 | 你親測手機版 + 桌面版 |
第四階段:上線後營運(永久)
| 步 | 誰 | 工具 | 做什麼 |
|---|---|---|---|
| 1 | 👤 | UptimeRobot | 你註冊 → 加網域 → 掛了寄 Email |
| 2 | 👤 | Cloudflare | 面板看 Analytics 流量 |
| 3 | 👤 | GA4 | 看訪客從哪來、看什麼頁面 |
| 4 | 👤 | Search Console | 看關鍵字排名、哪些頁被收錄 |
👤 = 必須你操作(涉及帳號、信用卡、密碼、肉眼判斷) 🤖 = AI 可代勞(寫碼、檢查、產出報告)
[[#table-of-contents|← 回目錄]]
第 2 章:部署一條龍
👤 你:操作 Cloudflare 面板、設定 DNS — 🤖 AI:產出 config 檔案、檢查設定、警示潛在錯誤
先通車,再裝潢。 先讓網站可以上線,再回來寫頁面內容。否則做完 13 頁才發現部署壞了 = 浪費一整天。
所有站都用同一套流程部署。差別只在要不要加 OAuth Worker + Routes。
DNS 原理 — 知道為什麼才不會矇查
你買了 minglab.tw → 這只是一個名字,沒有人知道這名字指向哪裡。
瀏覽器打 minglab.tw
↓
DNS 查詢:minglab.tw 的 IP 是?
↓
Cloudflare 回答:在我這裡,我是 proxy
↓
Cloudflare 處理 HTTPS / CDN / DDoS → 把請求轉給你的靜態網站
↓
瀏覽器顯示網頁
| 情境 | 做法 | 為什麼 |
|---|---|---|
| Cloudflare Pages(你的網站) | 用 Custom Domain 綁定,自動設 CNAME | Pages 幫你處理 DNS,不用手動加 A 記錄 |
| Cloudflare Worker(OAuth) | 用 Workers Routes,自動處理 | Worker 自己接 api/* 路徑 |
| 網域不在 Cloudflare 註冊 | 去原註冊商把 nameserver 指向 Cloudflare | DNS 託管權要轉移到 Cloudflare |
| DNS 改了沒生效 | 等 1-5 分鐘 | DNS 傳播有延遲,不是即時 |
最常見的 DNS 錯誤:A 記錄指向舊主機 IP
如果你在 Cloudflare DNS 面板看到 A 記錄 minglab.tw → 某個 IP,但網站是部署在 Pages 上 → 這兩者在打架 → 網站打不開。解法:刪掉 A 記錄,讓 Pages 的 Custom Domain 自己處理。
A.1 建立 Private Repo
- github.com/new
- Name:
專案名稱 - 選 Private
- 不要勾 Initialize with README
- Create → 記下網址
| 陷阱 | 解法 |
|---|---|
| 名稱跟現有 repo 衝突 | 換一個名字或用 -v2 後綴 |
| 忘了設 Private | 去 Settings → 改成 Private |
| 不小心勾了 Initialize | 會產生 main 分支衝突 → 用 git pull --rebase 解決 |
A.2 SSH Key 設定(多帳號管理)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "信箱"
cat ~/.ssh/id_ed25519.pub
把輸出的內容貼到 github.com/settings/ssh/new
多帳號設定(如 gimmi520 + minglabtw):
# 第二組 Key
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_第二帳號 -N "" -C "第二信箱"
cat ~/.ssh/id_ed25519_第二帳號.pub
# 貼到第二個 GitHub 帳號的 Settings → SSH Keys
| 陷阱 | 解法 |
|---|---|
SSH Host key verification failed | ssh-keyscan github.com >> ~/.ssh/known_hosts |
| 推到舊帳號 | 確認 git remote -v 的 URL 是 git@github.com:正確帳號/正確repo.git |
| 兩個帳號的 SSH Key 衝突 | Git 預設只會用 id_ed25519。多帳號需要在 ~/.ssh/config 加 Host github.com-第二帳號 並用 IdentityFile 指向不同的 Key |
config.yml 的 repo: 寫錯帳號 | Sveltia CMS 靠 config.yml 決定內容存到哪個 repo。repo: gimmi520/minglab-website — 帳號跟 GitHub 上拼法必須完全一致(含大小寫、連字號)。見 [[#trouble-git-remote |
A.3 Git Init + First Push
git init && git add . && git commit -m "初始版本"
git branch -M main
git remote add origin git@github.com:帳號/專案名.git
git push -u origin main
| 陷阱 | 解法 |
|---|---|
remote origin already exists | git remote set-url origin git@github.com:新帳號/新repo.git |
| Push 被 reject(遠端有新 commit) | git pull --rebase && git push |
忘記 git init | 整個目錄重新來 |
A.4 Push 前檢查清單
# 1. 確認 git remote 推對帳號
git remote -v
# 2. 確認 config.yml repo 名稱正確
grep "repo:" public/admin/config.yml
# 3. 確認沒有殘留舊 GitHub Pages 部署檔
ls .github/workflows/deploy.yml 2>/dev/null && echo "❌ 多餘!應刪除" || echo "✅"
# 4. 確認 .gitignore 有排除 node_modules/dist/.astro
cat .gitignore
[[#table-of-contents|← 回目錄]]
Part B:本地 → GitHub(後台設定)
B.1 Sveltia CMS 檔案結構
public/admin/
├── index.html ← Sveltia CMS 入口(CDN 載入)
└── config.yml ← 所有 collection 定義
B.2 config.yml 完整範本
backend:
name: github
repo: gimmi520/專案名 # ⚙️ 改這裡
branch: main
base_url: https://你的網域 # ⚙️ 改這裡
auth_endpoint: /api/auth
locale: 'zh_TW'
media_folder: "public/images"
public_folder: "/images"
collections:
- name: "blog"
label: "部落格"
folder: "src/content/blog"
create: true; extension: "md"; format: "frontmatter"
fields:
- { name: "title", label: "標題", widget: "string" }
- { name: "body", label: "內文", widget: "markdown" }
| 陷阱 | 解法 |
|---|---|
| repo 名稱忘記改 | 用 grep "repo:" public/admin/config.yml 檢查 |
YAML 縮排錯誤(fields: 跑掉) | 用 2 空格縮排,fields: 必須在 collection 下縮 4 格 |
create: false 讓 collection 消失 | Sveltia CMS 需要 create: true 才會顯示 |
B.3 index.html(CDN 載入)
<body>
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
<div class="admin-btns">
<a href="/admin-guide">?</a>
<a href="/">🏠</a>
</div>
</body>
B.4 Content .md 範例
每個 collection 的資料夾至少放一個 .md 範例,否則 Sveltia CMS 不會顯示該 collection。
src/content/blog/hello-world.md ← 任何一篇初始文章
src/content/about/index.md ← 關於我(至少一筆)
B.5 AGENTS.md + PROJECT_NOTES.md
# 專案名稱
## Stack
- Astro v6 + Tailwind CSS v4
- Sveltia CMS(CDN)
- Cloudflare Workers
## Dev commands
npm run dev # http://localhost:4321
npm run build # 輸出到 dist/
B.6 GitHub OAuth App 建立
| 步驟 | 操作 |
|---|---|
| 1 | GitHub → Settings → Developer settings → OAuth Apps → New OAuth App |
| 2 | Application name:專案名稱-cms |
| 3 | Homepage URL:https://你的網域 |
| 4 | Authorization callback URL:https://你的網域/api/callback(只放一個) |
| 5 | Register → 複製 Client ID + Client Secret |
每個網站要有獨立的 OAuth App,不要共用。
| 陷阱 | 解法 |
|---|---|
| 多個 callback URL | GitHub 可能選錯,只用一個專屬該網域的 URL |
| Client Secret 洩漏 | 只放進 oauth-worker/index.js,不要 commit 到 repo |
B.7 建立 Token 登入(客戶第一次使用)
這是客戶登入後台的方式。做一次,瀏覽器記住,之後不用再貼。
你幫客戶做的(一次性)
| 步 | 操作 | 說明 |
|---|---|---|
| 1 | GitHub → 右上頭像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token | github.com/settings/tokens/new |
| 2 | Note: 客戶名-網站名 | 以後知道這個 Token 是誰的 |
| 3 | Expiration: No expiration | 客戶不會半年後突然被鎖 |
| 4 | 勾選 repo + workflow | 這兩個就夠了 |
| 5 | Generate → 立刻複製(離開頁面後看不到) | 貼到 1Password / KeePass / LINE 存檔給客戶 |
客戶第一次用
| 步 | 操作 |
|---|---|
| 1 | 打開 https://網站/admin/ |
| 2 | 點 Token 按鈕 |
| 3 | 貼上你給的 Token → 登入 |
之後瀏覽器會記住 Token(localStorage),下次打開 /admin/ 直接進後台,不用再貼。
[[#table-of-contents|← 回目錄]]
Part C:Cloudflare 部署(前台)
C.1 部署流程(所有站通用)
不管你網站有沒有 OAuth 登入,前面 5 步完全一樣。
| 步 | 誰 | 做什麼 | 說明 |
|---|---|---|---|
| 1 | 👤 | GitHub 開 Private repo | 存放原始碼。忘了設 Private → 去 Settings 改成 Private |
| 2 | 👤 | GitHub → Settings → Applications → Cloudflare Pages → Repository access → 勾選新 repo | 每個新 repo 都要來一次。 漏掉 = Cloudflare 看不到 |
| 3 | 👤 | Cloudflare → Workers & Pages → Create → Continue with GitHub → 選 repo → Deploy | 直接部署,不用改任何設定 |
| 4 | 👤 | 等 1-2 分鐘 → 確認網站正常 | 打開 URL 看內容正確 |
| 5 | 👤 | Custom Domain → 輸入 你的網域 | Cloudflare 自動建 DNS,不需進 DNS 面板 |
第 5 步之後:要不要加 OAuth
| 無 API / Token 登入 | 有 API / OAuth 登入 | |
|---|---|---|
| 下一步 | ✅ 完成 | ⚠️ 還要加以下 |
| OAuth Worker | ❌ 不需要 | ✅ 見 §D.1 |
| Routes | ❌ 不需要 | ✅ 見 §D.3 |
| DNS | ❌ 已完成 | ❌ 已完成(第 5 步自動處理) |
Token vs OAuth — 選哪個
| Token 登入 | OAuth 登入 | |
|---|---|---|
| 部署後還要做什麼 | 0 步,直接完成 | 加 Worker + Routes |
| DNS | Custom Domain 自動 | 同 |
| 客戶體驗 | 貼 Token,瀏覽器記住 | 點 GitHub |
| 維護 | 零 | 需維護 Client ID/Secret |
預設走 Token。零額外設定。客戶不需要知道 GitHub 是什麼。
C.2 Custom Domain 詳解
為什麼不用手動設 DNS
Cloudflare 偵測到你輸入的網域(如 sop.minglab.tw)在你的帳號下 → 自動建 CNAME → 自動申請 HTTPS。你只需輸入網域、點 Add。
你做的事:輸入 `sop.minglab.tw` → 點 Add → 等 1-2 分鐘
Cloudflare 自動做的事:
① 檢查 minglab.tw DNS 在你帳號下 → 是
② 自動建 CNAME: sop → xxx.workers.dev
③ 自動申請 HTTPS 憑證
④ 生效
什麼時候才需要手動設 DNS
| 情況 | 做法 |
|---|---|
| 網域在別的 Cloudflare 帳號下 | 去原帳號設 CNAME 指向你的 xxx.workers.dev |
| 網域剛從別處轉入 Cloudflare | 等 nameserver 生效再綁 Custom Domain |
| 以上都不符合 | 不用手動設任何東西 |
最常見的三個卡關點
| # | 卡在哪 | 原因 | 解法 |
|---|---|---|---|
| 1 | Cloudflare 看不到 repo | GitHub Applications 沒勾選新 repo | 回步驟 2 |
| 2 | Deploy 失敗(npx wrangler) | Framework 被自動猜錯 | 重新 Deploy → 手動設 Framework: None |
| 3 | Custom Domain 綁不上去 | 舊 DNS 記錄卡住 | 刪掉舊記錄,等 1 分鐘再試 |
C.3 Build 設定
| 欄位 | 值 | ⚠️ |
|---|---|---|
| Framework preset | None | ← 關鍵!不要選 Astro |
| Build command | npm run build | |
| Build output directory | dist | |
| Deploy command(如有) | 留空 |
選 Astro 會強制跑 npx wrangler deploy → 需要 adapter → 失敗。
C.4 Connect GitHub → 自動部署
- Cloudflare → Workers & Pages → Create application → Continue with GitHub
- 授權 → 選 repo → Begin setup
| 陷阱 | 解法 |
|---|---|
| Cloudflare Pages 找不到新 repo(最高頻率卡關) | 每開一個新 GitHub repo 都要來這裡:GitHub → 右上頭像 → Settings → Applications → Cloudflare Pages → Configure → Repository access → 搜尋新 repo → 勾選 → Save。沒勾 = Cloudflare 看不到 = 無法部署 |
| 點了 Continue with GitHub 直接跳到 Workers | repo 有舊 Workers 歷史 → 開新 repo |
C.5 Custom Domain 綁定
Pages 專案 → Custom domains → 輸入網域 → Activate
C.6 HTTPS / SSL 確認
Cloudflare 自動配發 SSL 憑證。Proxy 必須開橘雲。
curl -sI https://你的網域 | grep HTTP
# HTTP/2 200 ← 正常
[[#table-of-contents|← 回目錄]]
Part D:OAuth 模式部署(模式 B 專用)
以下為模式 B(OAuth 登入)專用。如果你選 Token 登入(模式 A)或純展示(模式 C),Part D 整段跳過。
OAuth 模式總覽
| 順序 | 做什麼 | 在哪裡 | 說明 |
|---|---|---|---|
| 1 | 建主站 | git push → Cloudflare 自動部署 | 你的 Astro 網站(npm run build → dist/) |
| 2 | 建 OAuth Worker | Workers → Create → Start with Hello World! | 獨立 Worker,只處理 GitHub 登入 |
| 3 | 貼程式碼 | Edit code → 貼 oauth-worker/index.js | 確認 Client ID + Secret 正確 |
| 4 | 設 Routes | 網域/api/* → OAuth Worker / 網域/* → 主站 | api/ 必須在上面* |
| 5 | Custom Domain | DNS 自動處理 | 輸入網域即可,Cloudflare 自動建 CNAME |
| 6 | Custom Domain + HTTPS | Workers & Pages 設定 | 等 1-2 分鐘 |
D.1 OAuth Worker 建立
- Workers & Pages → Create → Workers → 選 Start with Hello World!
- 名稱:
專案名-oauth - Deploy → Edit code
| 陷阱 | 解法 |
|---|---|
| ❌ 選 Connect to Git | 會變成完整專案 → 跟主站衝突 |
| ✅ 選 Start with Hello World! | 獨立 Worker,只處理 OAuth |
D.2 OAuth Worker 程式碼
# 複製模板中的 oauth-worker/index.js 全部內容
# 貼到 Cloudflare 編輯器
# 確認第 17-18 行的 Client ID + Secret 是正確的
| 陷阱 | 解法 |
|---|---|
| 只按 Save 沒按 Deploy | 必須 Save → Deploy → Save and Deploy 兩個按鈕 |
| Client ID 是舊的 | curl -sI https://網域/api/auth 檢查 client_id= |
D.3 Workers Routes 設定
| 優先 | Route | Worker | 說明 |
|---|---|---|---|
| 1 | 網域/api/* | 專案名-oauth | OAuth 必須在上面 |
| 2 | 網域/* | 專案名 | 主站在下面 |
| 陷阱 | 解法 |
|---|---|
api/* 指錯 Worker | 從另一個專案的 OAuth 複製時忘記改 |
api/* 放在 * 下面 | /api/callback 永遠被主站攔截 → OAuth 失敗 |
D.4 後台登入測試
| 測試 | 網址 | 預期 |
|---|---|---|
| 後台畫面 | https://網域/admin/ | Sveltia CMS 三個按鈕 |
| OAuth 回呼 | https://網域/api/auth | 302 跳轉 GitHub 授權 |
| Token 登入 | 後台點 Token → 貼上 PAT | 進入 CMS 編輯頁面 |
D.5 Cloudflare API Token(給 OpenCode 代理用)
| 步驟 | 操作 |
|---|---|
| 1 | dash.cloudflare.com/profile/api-tokens → Create Token |
| 2 | Custom Token |
| Permissions | 值 |
|---|---|
| Account → Cloudflare Pages | Edit |
| Account → Workers Scripts | Edit |
| User → User Details | Read |
- Account Resources → 選你的帳號 → Create
D.6 wrangler CLI 基本用法
# 登入驗證
CLOUDFLARE_API_TOKEN=你的token npx wrangler whoami
# 部署 Pages(靜態網站)
npx wrangler pages project create 專案名 --production-branch main
npx wrangler pages deploy dist/ --project-name=專案名
# 部署 Worker(API)
npx wrangler deploy --name worker名稱
[[#table-of-contents|← 回目錄]]
Part E:部署後驗證清單
E.1 全站頁面 200 檢查
# 每個頁面一條
for page in "" about contact products blog admin-guide; do
curl -s -o /dev/null -w "%{http_code}" "https://網域/$page/"
echo " /$page"
done
E.2 後台 CMS 可編輯
✅ /admin/ → 三個登入按鈕(GitHub / Token / Local)
✅ Token 登入 → 進入編輯頁面
✅ Collection 列表正確、可新增/編輯/刪除
E.3 OAuth 回呼正常
curl -sI https://網域/api/auth | grep -i location
# location: https://github.com/login/oauth/authorize?... ✅
E.4 聯絡表單 mailto 測試
填寫表單 → 點送出 → 應打開信箱 → 內容正確
E.5 手機版檢查
| 檢查 | 重點 |
|---|---|
| 導航選單 | 漢堡選單可開關 |
| 對比表 | 卡片顯示不需左右滑 |
| 聯絡頁 | 三張卡片 + 表單正常 |
| 按鈕 | 不會被底部擋住 |
E.6 SEO 基礎檢查
curl -s https://網域/robots.txt
curl -s https://網域/sitemap-index.xml | head
curl -s https://網域/rss.xml | head
第 3 章:定義好規則,才開始寫程式
[[#table-of-contents|← 回目錄]]
Part F:緊急救援
部署後出問題,從上往下檢查,不要反過來:
| 順序 | 檢查 | 指令 |
|---|---|---|
| ① | DNS | dig 網域 +short |
| ② | Routes | Cloudflare 面板 → Workers Routes |
| ③ | 部署狀態 | Cloudflare → Deployments |
| ④ | config.yml | grep "repo:" public/admin/config.yml |
| ⑤ | 頁面標題 | grep "Layout title=" src/pages/*.astro |
| ⑥ | 殘留檔案 | ls .github/workflows/deploy.yml |
| ⑦ | 程式碼 | 最後才檢查 |
詳見 [[#trouble-deploy-order|§7.17 部署除錯優先順序]]
Part G:Email Routing — 免費專業信箱
Cloudflare Email Routing 讓你用 info@你的網域 收信,自動轉到你的 Gmail。
前提條件
| 條件 | 檢查 |
|---|---|
| 網域 DNS 在 Cloudflare 管理 | nameserver 指向 Cloudflare |
| Cloudflare Proxy 已開 | DNS 記錄旁是橘雲圖示 |
| 有一個真實信箱當目的地 | Gmail / Yahoo 任何 Email |
限制
Email Routing 只能收信(轉到 Gmail),不能直接寄信。要從 info@你的網域 寄出,在 Gmail 設定「寄件地址」(免費,5 分鐘)。
設定步驟
① Cloudflare 面板 → 你的網域 → Email → Email Routing → Get started
② Add destination → 輸入你的 Gmail → 收驗證信 → 點確認連結
③ 回到 Cloudflare → Routes → Add route
→ Custom address: 填 info(前面框,不要 @,系統自動帶網域)
→ Action: Send to an email
→ Destination: 輸入你的 Gmail
④ 點儲存 → 完成
驗收測試
用另一個信箱寄一封信到 info@你的網域,看你的 Gmail 收到沒。
| 測試結果 | 對策 |
|---|---|
| ✅ 收到 | 完成 |
| ⚠️ 收到但在垃圾郵件 | 正常。 新網域前幾封信可能被 Gmail 歸類為可疑。手動點「這不是垃圾郵件」,用幾次就會改善 |
| ❌ 沒收到 | 檢查 MX 記錄 → dig 你的網域 MX +short → 應回傳 route.mx.cloudflare.net |
從 Gmail 寄出(選配)
Gmail → 右上齒輪 → 查看所有設定 → 帳戶和匯入
→ 「以這個地址寄送郵件」→ 新增另一個電子郵件地址
→ 輸入 info@你的網域
→ SMTP 伺服器: smtp.gmail.com
→ 你的 Gmail 帳號 + 應用程式密碼
→ 驗證 → 完成
設定完後做的
| 動作 | 說明 |
|---|---|
把網站聯絡頁的 mailto 改成 info@你的網域 | 專業感 +100 |
| 測試寄信 → 確認 Gmail 收到 | 30 秒 |
| 告知客戶他的專業信箱 | 「你好,你的信箱是 info@你的網域」 |
關於 mailto 按鈕的注意事項
手機點「聯絡我」按鈕時,如果沒裝郵件 App 會跳「沒有預設郵件程式」。這是 mailto: 連結的原罪,不是網站 bug。未來可用 Formspree 表單服務取代(免費 50 封/月)。
[[#table-of-contents|← 回目錄]]
第 3 章:定義好規則,才開始寫程式
👤 你:定義設計規則、審核規範表 — 🤖 AI:建 AGENTS.md + 視覺規範表,跑審計腳本
這章是你跟 AI 的「前置合約」。規則訂清楚,後面 10 章就不會偏離。 在所有頁面開始前必須先定義視覺規範。 一但前置對了 → 全部對。一但前置錯了 → 無止盡修 bug。
A. 前置作業哲學
在兩個真實專案(ming-travel-blog、ming-website)中,學到最重要的一件事:所有後續的 bug,都是因為一開始沒有定義清楚規則。
| 踩過的坑 | 根因 | 如果前置有規範 |
|---|---|---|
| 修了 4 次光暈問題 | 全站用了 5 種不同 shadow 公式 | 先規定「只用一種 0_0_20px」 |
| 修了 4 次 transition 不生效 | global.css 有 !important 100ms 覆蓋 | 先規定「禁止 !important 全局覆蓋」 |
| 修了聯絡頁 3 次格式 | 每次從零設計卡片布局 | 先規定「參考模板 contact.astro」 |
| 改了 N 次價格格式 | K 縮寫 vs 完整數字混用 | 先規定「永遠完整數字」 |
核心服務哲學:客戶永遠不用改變他們自己的習慣。是我們要做到他們無須學習成本就能達成。
這句話貫穿整份手冊的設計決策:
- 後台上傳照片會卡?→ 瀏覽器自動壓縮,不是叫客戶先壓縮
- 手機上看不到表格?→ 自動換成卡片格式,不是叫客戶轉橫向
- 不會 Markdown?→ 後台使用說明頁有完整教學,不是叫客戶去 Google
結論:先把規則寫死,OpenCode 從第一行程式碼就照著走。 規則不是限制,是讓每個人(你、AI、客戶)都知道邊界在哪裡。邊界內自由發揮,邊界外不會發生。
B. 開始前必填的視覺規範表
OpenCode 在寫任何程式碼之前,必須先產出這張表給你確認:
| # | 項目 | 值 | 範例 |
|---|---|---|---|
| 1 | 主色 | ___ | #E27D60(暖橘)或 #06B6D4(青色) |
| 2 | 輔色 | ___ | #E8A87C、#fcd34d |
| 3 | 亮背景色 | ___ | #FDFBF7 |
| 4 | 暗背景色 | ___ | #1C1917 |
| 5 | 標題字體 | ___ | Inter(粗體) |
| 6 | 內文字體 | ___ | Noto Sans TC |
| 7 | 卡片圓角 | ___ | rounded-2xl |
| 8 | hover 光暈公式 | ___ | hover:shadow-[0_0_20px_rgba(主色,0.15)] dark:hover:shadow-[0_0_20px_rgba(主色,0.25)] |
| 9 | transition 時間 | 1200ms | 全站只用這一個值 |
| 10 | badge 光暈風格 | ___ | rounded-md + dark:shadow-[0_0_15px_rgba(255,255,255,0.1)] |
| 11 | 背景 blur 規格 | ___ | <div class="absolute ... w-[300px] h-[300px] blur-[80px] bg-[主色]/5 ... -z-10"></div> |
規則:這張表填完,你確認後,才能開始寫第一行程式碼。每次建新頁面都從這張表取值。
B2. 手機優先設計原則(必須在開始設計前確定)
| # | 原則 | CSS / HTML 寫法 | 說明 |
|---|---|---|---|
| 1 | 對比表 | 桌上 <table> + 手機直式卡片,用 hidden md:block / md:hidden 切換 | 不要讓客戶左右滑表格 |
| 2 | 方案卡片 | grid-cols-1 md:grid-cols-3 | 手機單欄、桌面三欄 |
| 3 | 聯絡頁左右區塊 | 左側 flex flex-col h-full、右側表單 flex-1 | 確保手機上兩邊等高 |
| 4 | CTA 按鈕 | w-full sm:w-auto | 手機全寬、桌面自適應 |
| 5 | 手機最小字體 | @media (max-width: 768px) { font-size: 17px } | 不讓客戶瞇眼讀內容 |
| 6 | 背景 blur 數量 | 每個區塊最多 1 個 <div class="absolute ... blur-[80px] ..."> | 手機效能考量 |
每次開始新頁面時,在提示詞中加入:
「手機版和桌面版都要考慮。
對比表在手機上用直式卡片,不要用左右滑動表格。
方案卡片在手機上單欄、桌面三欄。
確保所有頁面在手機上字體不小於 16px。」
C. 技術規範(寫入 AGENTS.md)
以下 6 條規則必須寫入每個專案的 AGENTS.md,OpenCode 每次 session 都會讀到:
| # | 規則 | 違反的後果 |
|---|---|---|
| 1 | 禁止 global.css 有 !important 全局 transition-duration | 所有 per-element duration 設定失效,除錯極難 |
| 2 | 禁止價格用 K 縮寫 | 視覺不專業、客戶混淆 |
| 3 | 行銷頁禁技術名(GitHub、Astro、Sveltia、Home Assistant…) | 客戶比價、覺得「不就是買零件」 |
| 4 | 建完每頁就跑審計腳本(見段落 D) | 及早發現不一致 |
| 5 | push 前跑全站審計 | 確認 shadow / duration / tech-name 全部一致 |
| 6 | 每次結束存 PROJECT_NOTES | 下次 session 才能接續 |
D. 審計腳本(建完每頁就跑)
# 1. shadow 公式審計 — 應該只有 1 種輸出(0_0_20px)
2. transition 時間審計 — 不應該有 300ms/500ms(只保留 1200ms、700 圖像、200 opacity)
grep -rn “duration-300|duration-500” src/ –include=“*.astro”
3. global.css !important 檢查
grep -rn “!important” src/styles/global.css
4. 定價格式審計 — 不應該有「K」縮寫在價格附近
grep -rn “NT$.K” src/pages/ –include=“.astro”
5. 技術名洩漏審計 — 行銷頁(排除 blog)不該有技術名
grep -rn “GitHub|Astro|Tailwind|Sveltia|Cloudflare” src/pages/ –include=“*.astro” | grep -v blog
**時機:每次建完一個新頁面後。每次 git push 前。**
---
### E. 除錯 SOP
| 症狀 | 第一步 | 第二步 |
|------|--------|--------|
| 「改了 class 但完全沒效果」 | global.css 有 `!important`? | 硬重整 `Cmd+Shift+R` |
| 「線上版跟本地不一樣」 | Cloudflare 部署完沒?等 1-2 分鐘 | `curl -s https://網站 \| grep "關鍵字"` |
| 「有的卡片亮、有的不亮」 | 跑段落 D 審計 #1 — shadow 有幾種? | 看哪一頁漏了 glow class |
| 「hover 跳太快或太慢」 | 跑段落 D 審計 #2 — duration 值統一嗎? | 檢查 global.css 有無 `transition-duration: Xms !important` |
| 「後台進不去」 | `curl https://網站/admin/config.yml` — YAML 正常? | 檢查縮排(2 格、fields 在 collection 下) |
| 「照片上傳後消失」 | 等 2-3 分鐘 Actions 轉檔 | 檢查 .md 引用是否 .jpeg → .webp |
| 「按鈕連結 404」 | 該頁面是否已刪除/改名? | `grep -rn "href=\"/舊路徑\"" src/` |
#### 除錯黃金法則
① 先跑審計腳本(自動發現 80% 的問題) ② 比較本地 vs 線上:curl 確認部署狀態 ③ 比較兩站:正常的那站做對照組(如 ming-website 對照旅誌) ④ 一次只改一個變數,確認效果後再改下一個
---
### F. 起手式提示詞(貼入新 OpenCode session)
**完整版(包含所有關鍵規則):**
在開始寫任何程式碼之前,請根據我的規範先建立兩份文件:
① AGENTS.md:記錄技術堆疊 + 技術規範(6 條必須遵守) ② 視覺規範表:主色、輔色、字體、圓角、hover光暈公式、 transition時間、badge風格、背景blur規格、手機優先原則
填完後讓我確認。確認後才能開始寫頁面。
設計規範(必須遵守):
- 所有對比表在手機上用直式卡片(hidden md:block / md:hidden)
- 方案卡片手機單欄、桌面三欄(grid-cols-1 md:grid-cols-3)
- hover 光暈公式只用一種:hover:shadow-[0_0_20px_rgba(主色,0.15)]
- 全站 transition 只用一個值:duration-[1200ms]
- 手機最小字體 16px
每個頁面建完後,請跑審計腳本確認沒有違反規範。 詳細規範參考:SETUP_GUIDE.md §0.5 前置作業規範
**加上你想要的風格:**
我要開一個新的 [旅遊部落格] 網站。風格選 [日落暖橙色]。
(風格選項見附錄 C:🌿侘寂 🌊海洋 🖤黑白 🌸粉嫩 🌲森林 🌅暖橙 🏙️城市 🎨撞色)
**部署後檢查順序(出問題時照這順序查,不要先懷疑程式碼):**
① DNS(dig 網域 +short) ② Routes(Cloudflare 面板 → Workers Routes) ③ 部署狀態(Cloudflare → Deployments) ④ config.yml(grep “repo:” public/admin/config.yml) ⑤ 頁面硬編碼標題(grep “Layout title=” src/pages/*.astro) ⑥ 殘留舊部署檔(ls .github/workflows/deploy.yml) ⑦ 程式碼 bugs(最後才檢查)
---
# 卷二:開發與除錯(Development & Debugging)
## 第 4 章:OpenCode 協作 + 提示詞 {#opencode-guide}
> **👤 你:專案經理 — 決定做什麼、確認結果** — **🤖 AI:全端工程師 — 寫程式、跑審計、除錯**
>
> 你不是在「操作一個工具」,你是在跟一個全端工程師結對編程。你說需求,AI 寫程式。你測結果,AI 修問題。
> 以下每一節都標示了在這一步中,你該做什麼、AI 該做什麼。
### 4.1 Session 管理 {#opencode-session}
**第一次開新專案:**
我要建立一個 [網站類型] 網站。 使用 Astro + Tailwind + Sveltia CMS + Cloudflare 部署。 請先幫我建立 AGENTS.md,記錄技術堆疊和開發指令。 然後建立 PROJECT_NOTES.md 作為進度追蹤。
**每次開始新 Session:**
繼續 [專案名稱] 專案。路徑:[絕對路徑] 上次已完成:[摘要] 下一步要做:[任務] 請先確認 npm run dev 是否正常運行。
**Session 長度規則:**
| 情境 | 建議 | 原因 |
|------|------|------|
| 新功能開發 | 一個功能一個 session | context 乾淨,不會被舊程式碼干擾 |
| 除錯 | 一個 bug 一個 session | 太多錯誤訊息會讓 AI 混淆 |
| 微調(改色/改字/改間距) | 可以堆在同一個 session | 不影響架構 |
| 對話超過 30 輪 | 重開新的 | context 有上限,超過會遺忘前面的規則 |
| npm run dev 跑不起來 | 試第二次,不行就開新 session | 不要在同一個 session 裡試第三次 |
**跨 session 傳遞規則:** 每次結束貼這三行到 `PROJECT_NOTES.md`:
已完成:______ 卡住的地方:______ 下一步要做:______
下一個 session 第一句話貼這三行給 OpenCode。
**👤 你永遠不該交給 AI 的事:**
| 事 | 原因 |
|----|------|
| 輸入密碼、Token、信用卡號 | AI 對話可能被記錄 |
| 操作 Cloudflare 面板(刪除、改 DNS、Rollback) | 誤操作無法復原 |
| 註冊帳號(GitHub、Cloudflare、網域商) | 涉及簡訊/Email 驗證,AI 無法代勞 |
| 決定客戶報價 | AI 不知道你的成本、關係、客戶預算 |
| 決定何時 git push | 你決定何時部署、commit message 寫什麼 |
| **最終驗收**(肉眼判斷視覺、手機親測) | AI 沒眼睛,看不到瀏覽器渲染結果 |
**🤖 你該交給 AI 的事:**
| 事 | 原因 |
|----|------|
| 寫 .astro / .yml / .js 程式碼 | 全端工程師本職 |
| 跑審計(grep、audit.sh、lychee) | 自動化檢查不該人做 |
| 產出文件(AGENTS、PROJECT_NOTES、手冊更新) | AI 產文件比你快 |
| 對照新舊版本,提取教訓(§4.10 專案回顧) | AI 讀文件比你快 |
| 搜尋舊方法殘留(新入舊出) | grep 全文件,人做不到,漏掉一條就是 bug |
> **開新 session 前看這張表,3 秒判斷什麼用 OpenCode、什麼親手做。**
### 4.2 AGENTS.md 模板 {#opencode-agents}
```md
# 專案名稱
## Stack
- Astro v6 + Tailwind CSS v4
- Sveltia CMS(CDN)
- Cloudflare Workers
- Node >= 22.12.0
## Dev commands
npm run dev # http://localhost:4321
npm run build # 輸出到 dist/
## Key paths
| Path | Purpose |
|------|---------|
| src/content/blog/*.md | 部落格文章 |
| public/admin/config.yml | CMS 設定 |
## Deployment
git push → Cloudflare 自動部署
domain: https://xxx.xxx
每個 AGENTS.md 必須包含這條關鍵規則:
## 關鍵規則
- 新入舊出:任何修改都要搜尋並清除舊方法殘留,不允許新舊共存
4.3 測試循環 SOP
1. npm run dev → 本地確認
2. npm run build(0 error)
3. git add . && git commit && git push
4. 等 1-2 分鐘部署
5. 打開正式網址確認
6. 有問題 → 描述 + 回步驟 1
4.4 中斷後接續
繼續 ming-travel-blog 專案。
路徑:/Users/xxx/ming-travel-blog
上次完成:Lightbox 改用 <dialog>,手機和桌面正常。
現在問題:關於我頁面想換照片,但不確定流程。
請先看 PROJECT_NOTES.md 了解現狀,然後告訴我下一步。
4.5 黃金法則
| # | 法則 |
|---|---|
| 1 | 一次一件事,不要混雜需求 |
| 2 | 每句話 = 問題 + 期望 + 上下文 |
| 3 | 「試看看」信任循環:AI 改 → 你測 → 回報 |
| 4 | 精確描述觀測結果 |
| 5 | 每次結束都存 PROJECT_NOTES |
| 6 | AGENTS.md 是第一印象 |
| 7 | 犯錯直接說,比「再試試」有效 |
| 8 | 讓 AI 自己檢查:「build 報錯,幫我看問題」 |
| 9 | 做完掃技術名:grep -rn "GitHub|Astro|Sveltia|Zigbee|Home Assistant" src/pages/ --include="*.astro" | grep -v blog → 行銷頁不該出現這些字 |
| 10 | 改一處,查全部:改完 A 檔案後,搜尋所有引用 A 的檔案是否也該連動修改(如:改了 index.html 的壓縮邏輯,admin-guide.astro 是否也要更新說明) |
| 11 | 新入舊出,不留殘留:新方法寫入的同時,必須搜尋並清除舊方法的殘留。不是只加新的,舊的錯誤資料不清 = 手冊信用破產 = 後續 bug 修不完。搜 grep -rn "舊的寫法" 所有.md 所有.astro,找到就換掉 |
4.6 技術名防洩漏檢查
客戶看到「Home Assistant」「Zigbee」「Intel N100」這些技術名會有兩個問題:① Google 比價 ② 覺得「不就是買零件」。所有行銷頁面(about、products、web-dev、contact、index)都應該用品牌化命名(如 AiHub C1),部落格保留 SEO 關鍵字。
每次建完新頁面後執行:
# 掃描行銷頁是否有技術名洩漏
grep -rn "Home Assistant\|Zigbee\|ESP32\|Intel N100\|mmWave\|Sveltia\|Astro\|Tailwind\|Cloudflare Workers\|GitHub Pages" src/pages/ --include="*.astro" | grep -v blog
# 如果有輸出 → 要在行銷頁模糊化
# 部落格(src/pages/blog/)的結果正常,保留
品牌化命名原則:
- 產品/硬體:用代號(AiHub C1、AiSense S1)
- 後台系統:對客戶說「智能管理後台」不是「Sveltia CMS」
- 部署平台:對客戶說「全球高速節點」不是「Cloudflare Workers」
- 網站技術:對客戶說「純靜態網站」不是「Astro + Tailwind」
4.7 有效提示 vs 無效提示
| ❌ 無效 | ✅ 有效 |
|---|---|
| 「加一個燈箱」 | 「攝影集點照片跳出全螢幕,← → 切換 + Esc 關閉」 |
| 「修一下 bug」 | 「手機上分類選單打開後立刻關閉,請改用文字輸入」 |
| 「Footer 不對」 | 「Footer 寫著 GitHub Pages,我們部署在 Cloudflare」 |
| 「顏色怪怪的」 | 「暗色模式 blockquote 文字黑色看不到,改淺灰色」 |
4.8 回報問題格式
問題:手機上點分類選單會立刻跳掉
環境:iOS Safari / Chrome
步驟:後台 → 攝影集 → 編輯照片 → 點分類標籤
預期:打開下拉選單選分類
實際:選單打開後立刻消失
4.9 AI 卡住時的處理 SOP
AI 不是每次都一次到位。當它開始重複、幻覺、或產出錯誤程式碼時,你需要一個中斷流程。
① 症狀判定
| 症狀 | 診斷 | 對策 |
|---|---|---|
| 同一段程式改了 3 次還是錯 | 不是你的問題,是提示詞沒說清楚 | 回到②中斷 |
| AI 開始重複產出相同內容 | context 爆了,遺忘前面的規則 | 回到②中斷 |
| AI 說「我理解了」然後產出無關的東西 | 幻覺 | 回到②中斷 |
| build 一直報錯,AI 修的都不是對的地方 | AI 沒有真正理解問題 | 回到③重來 |
② 中斷手段(依強度排列)
| 強度 | 手段 | 說明 |
|---|---|---|
| 輕 | 貼入:「你現在重複產出相同內容,請停止。我要重新描述問題。」 | 讓 AI 意識到它卡住了 |
| 中 | /clear | 清掉對話,從頭重新給提示詞 |
| 強 | Ctrl+C → 開新 OpenCode session | 全新 context,貼 PROJECT_NOTES 最後一段 |
③ 重來策略(不是重來整個專案,是重來「這一段對話」)
上一個 session 做到 PROJECT_NOTES.md 第 47 項:Lightbox 手機版點背景關不掉。
之前的解法:用的是 <dialog> + close(),
但手機上點 backdrop 不會觸發 close。
請看 src/pages/gallery.astro 的 Lightbox 區塊,找到原因,
不要重寫整個檔案,只修問題點。
第 5 章:一頁一頁拆解
| 規則 | 原因 |
|---|---|
每做完一個功能就跑一次 npm run build | build 不過立刻回報,不要累積問題 |
| build 不過不繼續往下做 | 一個 bug 沒解完,不開下一個主題 |
| 每次只修一個東西 | 兩個 bug 一起貼,AI 會混淆 |
4.10 專案結束回顧 SOP
每做完一個網站,執行以下 15 分鐘流程。這不是額外工作,是讓下一個網站更快完成的投資。
流程
① 你對 AI 說:
「請讀 PROJECT_NOTES.md,對照 SETUP_GUIDE.md,
提取這次專案跟之前專案的差異中,
有哪些值得寫進手冊但還沒寫進去的?」
② AI 回傳 3-5 條建議,每條格式:
- 教訓:[一句話]
- 建議寫入:§X.X
- 原因:[為什麼這是可複製的經驗]
③ 你逐條確認:
☐ 寫進去 ☐ 暫緩(等更多案例) ☐ 不寫(太特殊)
④ AI 把確認的條目寫進手冊對應章節
案例:從 ming-website 提取的教訓
| 原始經驗 | 寫進手冊哪裡 |
|---|---|
產品對比表在手機上不能用 <table>,要同時做桌面表格 + 手機卡片兩個版本(hidden md:block / md:hidden) | §5.3 首頁設計原則 |
按鈕顏色用 string 欄位讓客戶手打 HEX → 改成 select 下拉(8 色可選) | §6.4 設計原則 |
| 行銷頁出現「Home Assistant / Zigbee / Intel N100」,客戶會自己 Google 比價,覺得「不就是買零件」 | §4.6 技術名防洩漏,補真實案例 |
為什麼這個流程重要
第一個網站 → 手冊有 100% 的「做了什麼」 第二個網站 → 手冊有 100% 的「做了什麼」+ 80% 的「差在哪」 第五個網站 → 手冊開始出現「預測」:接到新案,AI 在你開工前就說「這個客戶需要產品對比表,見 §5.3;這個客戶是科技業,注意 §4.6 技術名洩漏」
記得好,才能越做越快。 PROJECT_NOTES.md 不是工作日誌,是教訓日誌:
❌ 不好的記錄:
「今天做了產品頁,完成」
✅ 好的記錄:
「產品頁完成。學到:對比表在手機上不能用 <table>,
要同時做兩個版本(桌面表格 + 手機卡片)。
之前以為 responsive 會自動處理,結果不會。
解法:hidden md:block / md:hidden 兩套並存」
[[#table-of-contents|← 回目錄]]
第 5 章:一頁一頁拆解
👤 你:決定架構、審美驗收 — 🤖 AI:產出 .astro 檔案、確保手機/桌面雙版正常
5.1 頁面總覽(13 頁)
| 路由 | 檔案 | 類型 | 說明 |
|---|---|---|---|
/ | index.astro | 動態 | 首頁 Hero + 3 卡片 + 最新 3 篇 |
/blog | blog/index.astro | 動態 | 部落格文章列表 |
/blog/[...slug] | blog/[...slug].astro | 動態 | 文章 detail |
/destinations | destinations.astro | 動態 | 目的地卡片列表 |
/destinations/[slug] | destinations/[slug].astro | 動態 | 目的地 detail |
/gallery | gallery.astro | 動態 | 攝影集 + Lightbox |
/about | about.astro | 動態 | 關於我(CMS collection) |
/contact | contact.astro | 靜態 | 聯絡表單(mailto) |
/admin-guide | admin-guide.astro | 靜態 | 後台使用說明 |
/404 | 404.astro | 靜態 | 404 頁面 |
/privacy | privacy.astro | 靜態 | 隱私權政策 |
/terms | terms.astro | 靜態 | 使用條款 |
/rss.xml | rss.xml.ts | 動態 | RSS Feed |
5.2 核心架構:Layout.astro
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const siteUrl = 'https://你的網域';
const { title, description, image } = Astro.props;
---
<head>
<title>{title}</title>
<meta property="og:title" content={title} />
<meta property="og:image" content={`${siteUrl}${image}`} />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" />
<script type="application/ld+json" set:html={...} />
第 6 章:後台設定
5.3 首頁(index.astro)
- Hero 區塊:主標題 + 副標題 + CTA 按鈕
- 三大卡片:硬編碼(不變的內容)
- 最新遊記:
getCollection('blog')動態抓前 3 篇
5.4 部落格列表(blog/index.astro)
- 日期格式:
toLocaleDateString('zh-TW', { year: 'numeric', month: 'long', day: 'numeric' }) - 封面圖可選:
{post.data.image && <img ... />} - URL:
post.id.replace('.md', '')
5.5 部落格文章(blog/[…slug].astro)
- Breadcrumb 導航(首頁 → 旅遊日誌 → 文章標題)
- JSON-LD Article 結構化資料
const { Content } = await render(post)— Astro render 輸出 Markdown
5.6 目的地列表(destinations.astro)
- 卡片結構:padding 在內容層(非外層),圖片自然填滿
- 10 色下拉選單主題色
- 封面圖可選(沒圖直接從標題開始)
5.7 攝影集(gallery.astro)
<dialog>原生 Lightbox(細節見 [[#trouble-lightbox|§7.2 Lightbox 開發史]])define:vars傳照片資料到 client script- 9 色分類標籤、
stopPropagation只在標籤上
5.8 關於我(about.astro)
- CMS collection,
create: false; delete: false(唯一一筆) - 頭像有/無條件渲染、社群連結有填才顯示
5.9 聯絡表單(contact.astro)
mailto:零後端、autocomplete="name/email"- 左側只留座標,不重複顯示信箱
<form action="mailto:信箱@gmail.com" method="GET" autocomplete="on">
<input name="name" autocomplete="name" required />
<input name="email" autocomplete="email" required />
<textarea name="message" required></textarea>
</form>
[[#table-of-contents|← 回目錄]]
第 6 章:後台設定
👤 你:定義 collection 架構(客戶要管什麼內容) — 🤖 AI:產出 config.yml、對接前端資料
6.1 檔案結構
public/admin/
├── index.html ← Sveltia CMS 入口(CDN 載入)
└── config.yml ← 所有 collection 定義
6.2 index.html
<body>
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
<div class="admin-btns">
<a href="/admin-guide">?</a>
<a href="/">🏠</a>
</div>
</body>
6.3 config.yml 完整範本
backend:
name: github
repo: gimmi520/專案名 # ⚙️ 改這裡
branch: main
base_url: https://你的網域 # ⚙️ 改這裡
auth_endpoint: /api/auth
locale: 'zh_TW'
media_folder: "public/images"
public_folder: "/images"
collections:
- name: "blog"
label: "旅遊日誌"
folder: "src/content/blog"
create: true; extension: "md"; format: "frontmatter"
editor: { preview: true }
fields:
- { name: "title", label: "標題", widget: "string" }
- { name: "image", label: "封面圖", widget: "image", required: false }
- { name: "description", label: "文章簡介", widget: "text" }
- { name: "date", label: "發布日期", widget: "datetime" }
- { name: "tags", label: "標籤", widget: "list", required: false }
- { name: "body", label: "文章內容", widget: "markdown" }
第 7 章:問題解決方案
label: "目的地"
folder: "src/content/destinations"
create: true; extension: "md"; format: "frontmatter"
fields:
- { name: "title", label: "標題", widget: "string" }
- { name: "image", label: "封面圖", widget: "image", required: false }
- { name: "location", label: "地點", widget: "string" }
- { name: "description", label: "簡介", widget: "text" }
- { name: "highlights", label: "特色項目", widget: "list" }
- { name: "color", label: "主題色", widget: "select",
options: [{ label: "🟠 暖橘", value: "#E27D60" }, ...] }
- { name: "bestSeason", label: "適合季節", widget: "string" }
- { name: "avgCost", label: "平均花費", widget: "string" }
- { name: "featured", label: "顯示於首頁", widget: "boolean" }
- { name: "body", label: "詳細介紹", widget: "markdown" }
-
name: “gallery” label: “攝影集” folder: “src/content/gallery” create: true; extension: “md”; format: “frontmatter” fields:
- { name: “title”, label: “標題”, widget: “string” }
- { name: “image”, label: “照片”, widget: “image”, required: false }
- { name: “description”, label: “照片描述”, widget: “text” }
- { name: “category”, label: “分類標籤”, widget: “string”, hint: “夜拍、古都、沙漠、街拍…” }
- { name: “location”, label: “拍攝地點”, widget: “string” }
- { name: “device”, label: “拍攝裝置”, widget: “string” }
- { name: “order”, label: “排序”, widget: “number”, value_type: “int”, default: 99 }
-
name: “about” label: “關於我” folder: “src/content/about” create: false; delete: false; extension: “md” fields:
- { name: “name”, label: “姓名”, widget: “string” }
- { name: “avatar”, label: “頭像照片”, widget: “image”, required: false }
- { name: “tagline”, label: “個人標語”, widget: “string” }
- { name: “bio”, label: “簡介”, widget: “text” }
- { name: “instagram”, label: “Instagram”, widget: “string” }
- { name: “twitter”, label: “Twitter/X”, widget: “string” }
- { name: “body”, label: “詳細介紹”, widget: “markdown” }
### 6.4 重要設計原則 {#cms-principles}
| 原則 | 說明 |
|------|------|
| select → string | 手機上原生 `<select>` 會跳掉,改用 string + hint |
| create: false | 關於我只有一筆,禁止新增 |
| value_type: "int" | Sveltia 用 snake_case |
| { label, value } 格式 | select options 統一格式,手機才正常 |
| YAML 2 空格縮排 | fields 在 collection 下縮 4 格 |
| **上傳自動壓縮** | `public/admin/index.html` 內建 JS:攔截 >1MB 圖片,瀏覽器端自動縮到 1920px,客戶無需任何操作 |
| **Token 優先 OAuth** | 預設用 Token 登入後台(零 API = Pages 部署 = 更穩更快)。除非客戶要求 GitHub 一鍵登入,才走 OAuth(§2 C.1 三模式對比) |
> [[#table-of-contents|← 回目錄]]
---
## 第 7 章:問題解決方案 {#troubleshooting}
> **👤 你:描述觀測結果(精確!)** — **🤖 AI:照檢查清單抓錯、修程式碼**
>
> AI 不是通靈。你給的線索越精確,AI 修得越快。照 7.17 的優先順序查,不要跳步。
### 7.1 Astro script 機制陷阱 {#trouble-astro-script}
| 寫法 | Astro 行為 | onclick 能找到? |
|------|-----------|:--:|
| `<script>` | Hoist 到 head,打包成 module | ❌ |
| `<script is:inline>` | 原封保留 | ✅ 但 `{var}` 不處理 |
| `<script define:vars>` | 保留原位,包 module | ❌ function 是 module-local |
**解法:** `const fn = window.fn = function() {}` — 雙重綁定。
### 7.2 Lightbox 開發史(8 次失敗) {#trouble-lightbox}
| # | 方法 | 失敗原因 |
|:--:|------|------|
| 1 | DOMContentLoaded + querySelector | Script 在 Layout 外,被 Astro 吃掉 |
| 2 | onclick + 移到 Layout 內 | Script 被 hoist |
| 3 | is:inline | `{galleryData}` 變純文字 |
| 4 | define:vars | function module-local |
| 5 | JSON data tag + is:inline | 沒被 Astro 處理 |
| 6 | window.openLightbox | 成功!但 scroll lock 有問題 |
| 7 | touch-action: none | 手機卡住 |
| 8 | **`<dialog>`** | ✅ 一次完美 |
**結論:先找瀏覽器原生 API,不要自己手刻。** 詳見 [[#section-gallery|§5.7 攝影集]]。
### 7.3 手機分類選單跳掉 {#trouble-mobile-select}
改用 `string` widget + `hint`:
```yaml
# ❌ 手機會跳掉
- { widget: "select", options: ["夜拍", ...] }
# ✅ 手機正常
- { widget: "string", hint: "夜拍、古都、沙漠..." }
7.4 Footer 部署平台標示
grep -rn "GitHub Pages\|github pages" src/ public/
7.5 YAML 縮排錯誤
collections:
- name: "blog"
label: "旅遊日誌"
fields: # 4 格
- { name: "title", ... } # 6 格
7.6 DNS 不生效
dig 你的網域 +short
curl -I https://你的網域
Proxy 狀態:橘雲 vs 灰雲(SSL 相關)。
7.7 垃圾桶問題速查
| 症狀 | 解法 |
|---|---|
| 後台進不去 | YAML 縮排檢查 |
| valueType 報錯 | 改 value_type(snake_case) |
| 照片上傳後消失 | 等 2-3 分鐘 Actions 轉完 |
| 封面圖靠左不滿版 | padding 移到內層 |
| ClientRouter 跳動 | 移除 ClientRouter |
| Footer 佔太多空間 | mt-32 py-12 → mt-16 py-8,間距減半 |
| 按鈕連結 404 | 頁面刪除/改名後,檢查所有 href="/old-path" 是否還有效 |
7.8 Footer 空間比例修正
Footer 預設的 mt-32 py-12 會讓底部區塊跟內容區域之間有 128px 空白,佔太多版面。
統一修正:
Footer class: mt-32 → mt-16(-50% 外距)
py-12 → py-8(-33% 內距)
主列間距: mb-8 → mb-4(-50%)
底部區間距: pt-6 → pt-3(-50%)
改完後 Footer 佔的視覺比例減少約 40%。
暗色模式 <hr> 分隔線
ming-website 踩坑:白色分隔線在暗色模式下像雷射筆,極度刺眼。
<!-- ❌ 預設(暗色背景上太亮) -->
<hr />
<!-- ✅ 修正(暗色下低調) -->
<hr class="border-gray-300 dark:border-gray-700" />
原則:所有 <hr> 都要加 dark:border-gray-700。 兩站都適用。
7.9 CMS schema 同步檢查
CMS config.yml 和 content.config.ts 的 collection 定義必須同步:
| 問題 | 檢查方式 |
|---|---|
| CMS 有 collection 但 Astro 沒 schema | 資料寫入但前端抓不到 → 無聲失敗 |
| Astro 有 schema 但 CMS 沒 collection | 前端能顯示但客戶無法編輯 |
| 欄位名稱不一致 | config.yml 用 buttonText 但 schema 用 cta → 值存不進去 |
每次改 CMS config 後:
# 確認兩邊的 collection 名稱一致
grep "^ - name:" public/admin/config.yml
grep "'[a-z]*':" src/content.config.ts
7.10 git remote 確認
新專案常見問題:commit 推到錯誤的 GitHub 帳號。
每次開新專案後:
git remote -v
# 確認輸出指向正確的 repo(如 minglabtw/ming-website)
# 不是舊專案的帳號(gimmi520/ming-website)
config.yml 的 repo: 名稱陷阱
ming-website 踩坑:config.yml 寫 repo: gimmi520/minglab-website,但某次誤以為是 ming-website。名稱差一個單字 → CMS 無法存檔 → 客戶登入後台看到「儲存失敗」。
# config.yml(Sveltia CMS 靠這行知道要推到哪個 GitHub repo)
backend:
repo: gimmi520/minglab-website # ← 必須跟 GitHub 上拼法完全一致
檢查指令:
# 確認 config.yml 的 repo 跟 git remote 一致
grep "repo:" public/admin/config.yml
git remote -v
# 兩邊的「帳號/專案名」必須完全相同
改 config.yml 後要重新部署,不只改本地檔案。
7.11 部署後網站打不開
如果 Worker 部署成功但域名打不開,先檢查 DNS:
| 檢查 | 指令 / 位置 |
|---|---|
| DNS 有記錄嗎? | dig 你的網域 +short |
根域名(如 minglab.tw) | Cloudflare DNS → 加一條 A record @ → Proxy 開(橘雲) |
子域名(如 travel.xxx.tw) | Cloudflare DNS → 加一條 CNAME → Proxy 開 |
真實案例: ming-website 部署後 minglab.tw 打不開,根因是 DNS 沒有 A 記錄,流量根本沒進 Cloudflare。
7.12 OAuth 登入後空白畫面
| 檢查 | 說明 |
|---|---|
Routes api/* 指向正確的 OAuth Worker? | 不要指到別的專案的 OAuth Worker |
OAuth Worker 的 redirect_uri 有指定嗎? | 加 redirect_uri: \https://${hostname}/api/callback`` 確保 callback 回到正確網域 |
| GitHub OAuth App 的 callback URL 只有一個? | 多個 callback URL 時 GitHub 可能選錯,建議獨立 OAuth App |
真實案例: minglab.tw/api/* Route 指到 ming-travel-oauth(旅站的),callback 跑到 travel.minglab.tw,後台永遠收不到 token。
7.13 後台登入後「沒有權限」
public/admin/config.yml 的 repo 欄位名稱不對。
# 確認 repo 名稱跟 GitHub 一致
backend:
repo: gimmi520/正確的repo名
真實案例: repo 改名為 minglab-website 後忘記更新 config.yml,CMS 嘗試存取已刪除的舊 repo。
7.14 部署一直被誤判為 Workers
Cloudflare 會記住 repo 的部署類型。如果 repo 曾連過 Workers,重新建立 Pages 也會被強制用 Workers 模式。
| 解法 | 說明 |
|---|---|
| 開全新 repo(推薦) | 零歷史記錄,Cloudflare 正確判為 Pages |
| 不要在同一個 repo 上反覆重建 | 每次重建 Cloudflare 只會回到之前記住的模式 |
真實案例: ming-website repo 曾連過 Workers → 刪掉 → 新建 Pages → 仍被強制用 Workers。改為全新 repo minglab-website 後一次成功。
7.15 KV Namespace 衝突
@astrojs/cloudflare adapter 會自動建立 KV Namespace。如果刪除專案重建,舊 KV 殘留會報 already exists 錯誤。
| 解法 | 說明 |
|---|---|
| Cloudflare → Storage → KV → 刪除舊的 Namespace | 解綁後才能刪 |
| 先解綁 Worker 的 KV binding → 再刪 KV | Settings → Bindings → 移除 SESSION |
7.16 頁面標題沒更新
Layout.astro 改了預設 title,但某些頁面 <Layout title="舊標題"> 硬編碼覆蓋了預設值。
| 檢查 | 指令 |
|---|---|
| 所有頁面的 title prop | grep -rn "Layout title=" src/pages/ --include="*.astro" |
| 預設 title | 在 Layout.astro 的 title = '...' |
真實案例: Layout.astro 改了「銘誠科動」但 index.astro 硬編碼「銘於心」,部署後標題一直沒變。
第 8 章:測試與交付檢查
如果選 OAuth 模式(§2 Part D),部署步驟照正確順序走(DNS → Build → Connect → Custom Domain),這一節你永遠不會用到。Pages 模式不需要 Routes,此節不適用。 這是兩個專案各花了 2 小時才學到的教訓。
部署後出問題,從上往下檢查,不要反過來:
| 順序 | 檢查 | 指令 | 說明 |
|---|---|---|---|
| ① | DNS | dig 網域 +short | 域有沒有指向 Cloudflare? |
| ② | Routes | Cloudflare 面板 → Workers Routes | api/* 指對 Worker 嗎? |
| ③ | 部署狀態 | Cloudflare → Deployments | 最新部署成功嗎? |
| ④ | config.yml 名稱 | grep "repo:" public/admin/config.yml | repo 名稱對嗎? |
| ⑤ | 頁面硬編碼 | grep "Layout title=" src/pages/*.astro | 有沒有覆蓋 Layout 預設? |
| ⑥ | 殘留檔案 | ls .github/workflows/deploy.yml | 有沒有舊 GitHub Pages 部署? |
| ⑦ | 程式碼 bugs | 最後才檢查 |
真實案例:ming-website 部署後打不開,我們先檢查了程式碼(⑦),查了 2 小時才回頭發現 DNS 沒有 A 記錄(①)。如果按這個順序,5 分鐘就能找到問題。
7.18 手機後台上傳圖片
已自動處理。 public/admin/index.html 內建一段 JS(約 40 行),在所有 Sveltia CMS 檔案上傳之前攔截:圖片 > 1MB → Canvas 壓縮至 1920px / quality 0.8 → 再交給後台上傳。
原圖 8MB → 瀏覽器壓到 ~500KB → 手機 4G 秒傳 → GitHub → Actions 轉 WebP
客戶視角:什麼都不用做。 打開後台、選照片、儲存。不需要壓縮 App,不需要學 Shortcuts 捷徑,不需要用 LINE 中轉。
服務哲學:客戶永遠不用改變他們自己的習慣。是我們要做到他們無須學習成本就能達成。
7.19 Cloudflare Worker / OAuth 除錯
Worker 壞了的三種查法
| 方法 | 指令 / 位置 | 說明 |
|---|---|---|
| 看 Log | Cloudflare 面板 → Workers & Pages → oauth-worker → Logs | 看有沒有紅色錯誤,特別是 401 (token expired) 或 500 |
| 即時 tail | npx wrangler tail oauth-worker | 看即時請求 + 回應,適合正在除錯 |
| curl 模擬 | curl -v https://你的網域/api/auth | 確認是回 302 (正常導向 GitHub) 還是 500/502 (有問題) |
最常見的三個錯誤
| 症狀 | 原因 | 解法 |
|---|---|---|
| OAuth 點下去跳空白頁 | REDIRECT_URI 跟 GitHub OAuth App 設的不一樣 | 檢查兩邊的 callback URL:https://你的網域/api/auth/callback |
| 後台登入後 500 Internal Error | 環境變數沒設(GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET) | Cloudflare 面板 → oauth-worker → Settings → Variables |
後台 /admin/ 404 Not Found | Workers Routes 沒設定 api/* 指到 oauth-worker | Cloudflare 面板 → 你的網域 → Workers Routes → 確認 api/* → oauth-worker |
Worker 跟 Pages 的關係(一句話)
Pages 處理靜態網站(你的網頁內容)
Worker 處理 API(OAuth 登入、回呼)
兩者獨立部署,靠 Routes 串接:api/* → Worker,其他 → Pages
[[#table-of-contents|← 回目錄]]
卷三:交付與商業(Business & Operations)
第 8 章:測試與交付檢查
👤 你:照 checklist 驗收、手機/桌面親測 — 🤖 AI:跑自動化腳本、產出驗收報告
8.0 audit.sh — 交付前的最後一道門
前置條件(首次執行前,一次性):
brew install lychee # dead link 檢查
chmod +x audit.sh # 確保可執行
audit.sh 存在於專案根目錄。一個指令,5 階段檢查:
./audit.sh # 跑預設網域
./audit.sh https://新客戶.com # 跑指定網域
| 階段 | 檢查 | 沒做客戶會遇到 |
|---|---|---|
| 1/5 | dead link | 客戶點連結跳 404,覺得你網站壞了 |
| 2/5 | Sitemap 網域正確 | Google 收錄到舊網域,新站永遠搜不到 |
| 3/5 | dist/ 無 localhost 殘留 | 網站出現 http://localhost 連結,客戶點了什麼都看不到 |
| 4/5 | dist/ 無舊 GitHub Pages 殘留 | 舊部署網址遺留在網站上,客戶困惑 |
| 5/5 | 全頁面回 200 | 某頁掛了你不知道,客戶先發現 |
目標:5/5 全部通過才能交付。
audit.sh 常見錯誤解析
跑完 audit.sh 後,不用每個紅字都緊張。以下是判定標準:
| 錯誤 | 例子 | 判決 | 說明 |
|---|---|---|---|
| 外部連結 404 | home-assistant.io/硬體/ 文件搬家 | ✅ 可不修 | 非本站控制,原作者改網址了 |
| 內部 sitemap 多出頁面 | /services/ 在 sitemap 但頁面已刪 | ⚠️ 查後台 | 有人在 CMS 發了又刪,從後台查 |
| 動態路由 test-slug 404 | destinations/test-slug/ 回 404 | ✅ 可不修 | 腳本用假 slug 測試,沒這篇文章正常 |
| canonical URL 錯誤 | 頁面 <link rel="canonical"> 寫著 github.io | ❌ 必須修 | Google 會收錄到舊網域,新站永遠搜不到 |
| 頁面不回 200 | /about/ 回 500 | ❌ 必須修 | 客戶點了會看到錯誤 |
| localhost 殘留 | dist/ 裡有 wrangler.json 的 "ip":"localhost" | ✅ 可不修 | Cloudflare adapter 開發設定,非真實 leak |
| 外部連結 403 | raspberrypi.com/software/ 被伺服器拒絕 | ✅ 可不修 | 對方網站擋 bot 或限制存取,非本站問題 |
8.1 全站頁面檢查(自動化)
# 掃描所有 Astro 頁面,檢查是否返回 200
for f in $(grep -l "^---" src/pages/**/*.astro 2>/dev/null); do
fname=$(echo "$f" | sed 's/src\/pages//' | sed 's/\/index\.astro$//' | sed 's/\.astro$//' | sed 's/\[\.\.\.slug\]//' | sed 's/\[slug\]/test/')
[ -z "$fname" ] && fname="/"
code=$(curl -s -o /dev/null -w "%{http_code}" "https://你的網域$fname")
echo "$code $fname"
done
8.2 手機版檢查(手動)
| # | 檢查項目 | 方法 | 通過 |
|---|---|---|---|
| 1 | 導航選單 | 打開漢堡選單 → 點每個連結 → 確認跳轉正確 | ☐ |
| 2 | 對比表 | 確認是直式卡片,不是左右滑動表格 | ☐ |
| 3 | 方案卡片 | 確認手機上是單欄排列(非擠成超小多欄) | ☐ |
第 9 章:上線後營運
| 6 | 字體大小 | 所有文字 ≥ 16px(開發者工具測量) | ☐ | | 7 | 無橫向滾動 | 整個頁面不需左右拖拉 | ☐ | | 8 | 圖片不溢出 | 確認所有圖片在螢幕範圍內 | ☐ | | 9 | 表單可用 | 輸入欄位不需左右拖拉 | ☐ | | 10 | navigationGuard | 後台編輯頁離開時彈出確認框(頁面+新分頁都測) | ☐ |
8.3 桌面版檢查
| # | 檢查項目 | 方法 | 通過 |
|---|---|---|---|
| 1 | nav sticky | 捲動頁面,確認導航列固定在頂部 | ☐ |
| 2 | 對比表 | 確認是 <table> 格式,非手機卡片 | ☐ |
| 3 | hover 光暈 | 0_0_20px_rgba(主色,0.15) + duration-[1200ms] | ☐ |
| 4 | 所有 hover | 每個可點擊元素 hover 有過渡動畫 | ☐ |
| 5 | 暗色模式 | 切換暗色模式,區塊背景正確(非黑色) | ☐ |
| 6 | 圖片比例 | 確認照片無變形 | ☐ |
8.4 後台 CMS 檢查
| # | 檢查項目 | 說明 | 通過 |
|---|---|---|---|
| 1 | OAuth 登入 | /api/auth → 302 導向 GitHub | ☐ |
| 2 | Token 登入 | 用 Token 可正常進後台 | ☐ |
| 3 | 看得到內容 | 後台左側選單顯示所有 collection | ☐ |
| 4 | 新增文章 | 新增一篇測試文 → 發布 → 前台出現 | ☐ |
| 5 | 編輯文章 | 修改既有文章 → 發布 → 前台更新 | ☐ |
| 6 | 刪除文章 | 刪除測試文 → 前台消失 | ☐ |
| 7 | 上傳圖片 | 上傳一張圖片 → 出現在媒體庫 | ☐ |
| 8 | navigationGuard | 點「新建/編輯」→ 輸入內容 → 按 F5 → 是否彈確認框 | ☐ |
8.5 OAuth 檢查
| # | 檢查 | 指令 | 通過 |
|---|---|---|---|
| 1 | Worker 有部署 | Cloudflare Workers & Pages → oauth-worker | ☐ |
| 2 | 環境變數 | env.GITHUB_CLIENT_ID、env.GITHUB_CLIENT_SECRET、env.REDIRECT_URI | ☐ |
| 3 | 回呼路徑 | REDIRECT_URI = https://你的網域/api/auth/callback | ☐ |
| 4 | Routes 路徑 | https://你的網域/api/* → oauth-worker | ☐ |
8.6 交付清單(給客戶前最後確認)
| # | 項目 | 通過 |
|---|---|---|
| 1 | 網域可正常訪問 | ☐ |
| 2 | 後台網址可登入(https://網域/admin/) | ☐ |
| 3 | 把後台帳號/密碼存到客戶的 1Password 或 KeePass | ☐ |
| 4 | 後台使用說明頁(/admin-guide)可訪問且正確 | ☐ |
| 5 | 聯絡表單寄得到客戶信箱 | ☐ |
| 6 | OAuth 憑證屬於客戶(非共用) | ☐ |
| 7 | 告知客戶後台操作方式(新增/編輯/刪除) | ☐ |
| 8 | 告知客戶 30 天保固範圍(不包含新功能開發) | ☐ |
| 9 | 保固聯絡方式(Email / LINE / Telegram) | ☐ |
保固聲明模板: 「網站交付後提供 30 天技術保固,包含:部署異常修復、後台無法登入、頁面異常掛掉。不包含:新功能開發、內容填寫、SEO 優化。超過 30 天後依維護方案計費。」
[[#table-of-contents|← 回目錄]]
第 9 章:上線後營運
👤 你:決策營運方向、追蹤數據 — 🤖 AI:解讀 GA4 數據、提供 SEO 建議、產出監控告警
9.1 SEO 技術面:確保 Google 能找到你
每篇文章的 meta 公式(直接在 Sveltia CMS 填)
| 欄位 | 寫法 | 範例 |
|---|---|---|
| title | [關鍵字] - [站名],40-55 字 | 「2026 京都賞楓自由行攻略 - 銘的旅誌」 |
| description | 含 1 次關鍵字,120-155 字 | 「完整京都賞楓路線、花費、交通,附 2026 最新紅葉情報。」 |
| cover image | 1200×630 封面圖 | og-cover.webp |
| h1 標題 | 含 1 次關鍵字 | <h1>京都賞楓自由行攻略</h1> |
| 第一段 | 開頭 100 字含關鍵字 | 「每年 11 月,京都清水寺周邊進入…」 |
不要做的事(會扣分或除名)
| ❌ 不要 | 後果 |
|---|---|
| 標題塞滿關鍵字(keyword stuffing) | Google 會降排名 |
| 直接抄襲別人整篇文章 | 重複內容會被隱藏或除名 |
| 白色字體隱藏關鍵字 | 黑帽 SEO,站點可能被除名 |
| 文章內互連農場(PBN) | Google 已能偵測,無效且危險 |
Google Search Console 提交(上線後 24 小時內必做)
① 打開 https://search.google.com/search-console
② 選「網址前置字元」→ 輸入 https://你的網域
③ DNS 驗證(最穩定):
Cloudflare → 你的網域 → DNS → Records → 新增
類型: TXT 名稱: @ 內容: google-site-verification=xxxxxxxxxx
④ 等 1-2 分鐘 → 點驗證按鈕
⑤ 左側選單 → Sitemap → 輸入 sitemap-index.xml → 提交
⑥ 等 2-3 天 Google 開始爬取
9.2 SEO 策略面:讓客戶自己找到關鍵字
客戶不需要花錢請 SEO 顧問。教他們三步驟:
① Google 搜尋框 → 打字 → 看自動建議 → 這些就是真人搜的字
② 競品網站 → F12 開發工具 → <title> 標籤 → 看在打什麼關鍵字
③ 問 ChatGPT:「我是[XX行業],台灣客戶會用什麼關鍵字在 Google 找我?
給我 10 個長尾關鍵字,不要品牌名」
④ Search Console → 成效報表 → 看到哪些字已經帶流量 → 繼續寫這些主題
地方服務業加碼:Google 我的商家
餐廳、民宿、SPA、咖啡廳、工作室 → 去 business.google.com 建立商家檔案。在 Layout.astro 的 JSON-LD 加入經緯度後,Google 地圖會自動串聯網站。
關於「多久會排上去?」的誠實回答:
| 時間 | 會發生什麼 |
|---|---|
| 1-2 週 | Google 發現你的網站(搜 site:你的網域 確認) |
| 1-3 個月 | 開始有零星搜尋流量(長尾關鍵字先) |
| 3-6 個月 | 穩定排名(條件:持續發文、內容原創、沒有黑帽) |
| 6 個月+ | 關鍵字排名逐漸提升 |
如果有人跟你說「保證一個月排上第一頁」,他在騙你。 Google 不保證排名,只保證收錄(透過 sitemap)。
9.3 GA4 流量追蹤
取得 GA4 追蹤碼
① 打開 https://analytics.google.com
② 左下 ⚙️(管理)→ 資料串流 → 新增串流 → 網站
③ 輸入網域,點「建立串流」
④ 複製「評估 ID」(格式 G-XXXXXXXXXX)
⑤ 貼入 src/layouts/Layout.astro 的 <head>
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
30 天後回去看的三個數字
| 指標 | 在哪看 | 健康值 |
|---|---|---|
| 日均瀏覽次數 | GA4 → 報表 → 生命週期 → 流量開發 | > 10 |
| 哪些頁面最熱門 | GA4 → 報表 → 生命週期 → 參與 → 網頁和畫面 | — |
| 跳出率 | GA4 → 同上 | < 70% |
9.4 Lighthouse 效能檢查
每次部署後跑一次,確保效能沒退化:
① Chrome DevTools(F12)→ Lighthouse 頁籤
② Mode: Navigation
③ 勾選 Desktop 和 Mobile 各跑一遍
| 指標 | 目標 | Astro 靜態站正常表現 |
|---|---|---|
| Performance | ≥ 90 | 95-100 |
| Accessibility | ≥ 95 | 100 |
| Best Practices | ≥ 90 | 100 |
| SEO | = 100 | 100 |
常見扣分原因與對策:
| 扣分原因 | 解法 |
|---|---|
| 圖片過大(WebP > 200KB) | 後台重傳較小尺寸,或等 Actions 自動壓縮 |
圖片沒設 width / height | <img width="800" height="600" ...> |
| 字體未 preload | 加 <link rel="preload" href="/fonts/..." as="font" crossorigin> |
| 第三方 script 阻塞 | Astro 靜態站幾乎沒這個問題 |
9.5 多語系方案預告
目前模板不支援多語系。如果客戶需要:
| 方案 | 複雜度 | 適合場景 |
|---|---|---|
| 開兩個 repo(中文站 + 英文站) | 低 | 內容少的小站 |
Astro i18n routing(/zh/、/en/) | 中 | 內容多、需要前後台雙語 |
| 部落格文章用 AI 逐篇翻譯 | 低 | 先用手動檔著 |
建議等客戶真的有需求和預算再做,不要預先過度設計。
9.6 客戶關懷與長期維護
每季客戶關懷 SOP
每 3 個月,發一封關懷信給所有客戶。目的是:確認網站正常 + 提醒更新內容 + 自然引出追加需求。
主旨:[客戶站名] 網站第 X 季健檢報告
Hi [客戶名],
本季網站健檢報告:
✅ 網站正常上線中,SSL 憑證自動更新
✅ 後台可正常登入與編輯
✅ Google 收錄頁數:[N] 頁(Search Console 可查)
小建議:如果你這季有新作品/新文章,現在更新會讓 Google
更常來爬你的網站,搜尋排名會更好。
第 10 章:服務商品包裝
#### 網域到期管理
| 動作 | 說明 |
|------|------|
| 記錄所有客戶的網域和到期日 | 用一個 `domains.txt` 或 Google Sheet |
| 到期前 30 天提醒客戶續約 | 網域過期 = 網站下線,客戶會很慌 |
| 建議客戶開自動續訂 | Cloudflare / Namecheap 都支援 |
#### 追加需求評估 SOP
客戶說「想加一個功能」時,不要立刻答應。先問:
① 這個功能對你的客戶有什麼幫助?(不是對你有什麼幫助) ② 多少客戶會用到這個功能? ③ 不急的話,我可以幫你評估工時和費用?
→ 小功能(1-2 小時):直接做,算在月維護裡 → 中功能(半天):報追加費用 → 大功能(一天以上):建議開新合約
### 9.7 監控與警報 {#post-monitoring}
網站掛了,你要比客戶先知道。
| 工具 | 費用 | 設定方式 | 用途 |
|------|:--:|------|------|
| **UptimeRobot** | 免費 | 註冊→新增監控→輸入網域→5 分鐘檢查一次 | 掛了寄 Email |
| **Cloudflare Analytics** | 免費 | 面板→你的網域→Analytics | 看流量、5xx 錯誤 |
| **Better Uptime** | 免費 3min | 註冊→新增監控→輸入網域 | 掛了寄 Email + 打電話 |
**設定 UptimeRobot(5 分鐘搞定):**
① https://uptimerobot.com → 註冊(用 Google 帳號即可,不用 GitHub OAuth) ② Add New Monitor → HTTP(s) → 輸入 https://你的網域 ③ Monitoring Interval: 5 minutes ④ Alert Contacts: 你的 Email ⑤ 完成。以後網站掛掉你會在第一時間收到通知 ⑥ Status Page 可以跳過 — 那是給客戶看的公開頁面,自己監控不需要
### 9.8 備份與回復 {#post-backup}
`git push` 出去的 code 如果有 bug,上一版會被覆蓋。兩招回復:
| 方法 | 指令 | 適用 |
|------|------|------|
| **Cloudflare Rollback** | Cloudflare 面板 → Workers & Pages → 你的專案 → Deployments → 點前一版 → Rollback | 最快,30 秒回復 |
| **git revert** | `git revert HEAD --no-edit && git push` | 從 Git 端回復上一次 commit |
| **部署前檢查** | `npm run build` 確認 0 error 再 push | 預防勝於治療 |
> [[#table-of-contents|← 回目錄]]
---
## 第 10 章:服務商品包裝 {#service-packaging}
> **👤 你:定價策略、客戶談判、簽約** — **🤖 AI:提供比較話術、FAQ 答案、市場行情參考**
### 10.1 定價策略 {#packaging-pricing}
| 方案 | 內容 | 建議價格 |
|------|------|------|
| 基本版 | 部落格 + 目的地 + 聯絡 | NT$ 25,000-35,000 |
| 完整版 | + 攝影集 + 關於我 + 社群 | NT$ 35,000-50,000 |
| 旗艦版 | + 自訂設計 | NT$ 50,000-80,000 |
**一次性費用,之後零月費。**
### 10.2 服務包視覺化 {#packaging-visual}
┌──────────────────────────────────────┐ │ 旅遊部落格架站方案 │ │ │ │ ✅ 自訂網域(你的名字.com) │ │ ✅ 專業信箱(你的名字@你的網域) │ │ ✅ 後台管理系統(瀏覽器直接編輯) │ │ ✅ 手機 + 桌面完美顯示 │ │ ✅ 手機上傳自動壓縮(不用裝 App) │ │ ✅ 交付前自動化驗證(audit.sh) │ │ ✅ SEO 搜尋引擎優化 │ │ ✅ 永久免費全球 CDN 託管 │ │ ✅ 1 小時操作教學 │ │ ✅ 30 天免費技術支援 │ │ │ │ 💰 一次性費用:NT$ xxxxx │ │ 💰 每月費用:NT$ 0 │ │ 💰 每年費用:只有網域費 ~NT$300-800 │ └──────────────────────────────────────┘
#### 傳統架站 vs 你的方案 — 3 年成本對比
| 項目 | 傳統行情(年費) | 你的方案 | 客戶省下 |
|------|------|------|:--:|
| 主機/伺服器 | NT$3,000-36,000 | Cloudflare Pages 免費 | 100% |
| SSL 憑證 | NT$0-3,000 | Cloudflare 自動免費 | 100% |
| CDN 加速 | NT$0-6,000 | Cloudflare 內建免費 | 100% |
| DDoS 防護 | NT$0-12,000 | Cloudflare 內建免費 | 100% |
| **專業信箱** | **NT$2,400-3,600** | **Cloudflare Email Routing 免費** | **100%** |
第 11 章:模板啟動
| 自動備份 | NT$1,200-3,600 | GitHub 版本控管 | 100% | | 網站更新/維護 | NT$12,000-60,000 | 客戶自己改(不需工程師) | 100% | | 網域費 | NT$300-800 | 一樣 | 0% | | 3 年固定費用 | NT$60,300-426,000 | NT$900-2,400 | 98-99% | | 一次性建置 | NT$43,000-160,000 | NT$25,000-80,000 | 40-50% | | 3 年總計 | NT$103,300-586,000 | NT$25,900-82,400 | 82-86% |
這張表是你最大的談判武器。客戶看到「人家 3 年要花 10 萬,你只要 2.5 萬」,成交機率翻倍。
10.3 客戶溝通劇本
開場: 「我幫你建一個旅遊部落格,全部自動化。你只需要打開瀏覽器登入後台,像填表單一樣寫文章。儲存後 2-3 分鐘網站就更新。不需要工程師、不用付月費、永遠不會壞。」
客戶問「為什麼這麼便宜?」 「因為用的是現代靜態網站技術,沒有伺服器沒有資料庫。全世界最大的 CDN 免費幫你託管。你的網站跟 Google、Facebook 跑在同一條高速公路上。」
客戶問「多久能做好?」 「從簽約到上線約 3-5 個工作天,含 1 小時教學。」
客戶問「跟找設計公司比,差多少錢?」 「給你看一個數字:傳統架站 3 年要花 10 到 60 萬(主機、信箱、SSL、維護費),我們 3 年只要 2.5 到 8 萬,而且每年只有網域費 NT$300-800。省下來的錢夠你再做三個網站。」
10.4 交付項目清單
| 階段 | 交付物 |
|---|---|
| 簽約 | 需求確認表、報價單 |
| 開發中 | staging 預覽網址 |
| 上線 | 正式網址、後台帳號、使用說明 |
| 結案 | 原始碼(客戶擁有)、30 天保固 |
10.5 常見 FAQ
| 問題 | 回答 |
|---|---|
| 「為什麼儲存後不會馬上出現?」 | 全球 CDN 需要 2-3 分鐘同步 |
| 「照片會不會不見?」 | 存在 GitHub,永久保存 |
| 「可以放多少照片?」 | 約 200-500 張,足夠多年使用 |
| 「手機可以用嗎?」 | 後台和前台都支援手機 |
| 「不會 Markdown 怎麼辦?」 | 說明頁有完整教學 |
| 「會被攻擊嗎?」 | 靜態網站無資料庫,無法被 SQL injection |
10.6 定價格式規範
從真實專案經驗:價格格式來回改了 4 次(K 縮寫 → 完整數字 → 心理定價 → 折抵策略)。
強制規則:
| 規則 | 說明 | 範例 |
|---|---|---|
| 永遠完整數字 | 禁止 3K、10K 等縮寫 | ✅ NT$ 3,000-10,000 ❌ NT$ 3K-10K |
| 心理定價 | 尾數用 999 或非整數 | NT$ 9,999 起(不是 10,000) |
| 折抵策略 | 評估費可折抵施工費 → 提高轉換 | 評估費 NT$ 1,999(施工可折抵) |
| 一次寫入所有檔案 | 決定定價後同步改 .md + 對比表 + 方案卡 | 避免前後價格不一致 |
[[#table-of-contents|← 回目錄]]
第 11 章:模板啟動
👤 你:創建新專案、填入客戶品牌資料 — 🤖 AI:複製模板、置換品牌變數、跑 npm install
11.1 啟動腳本(setup.sh)
#!/bin/bash
read -p "專案名稱:" name
read -p "客戶網域:" domain
read -p "GitHub 帳號:" gh_user
read -p "聯絡信箱:" email
附錄 A:完整檔案對照表
git clone git@github.com:你的帳號/模板repo.git temp cp -r temp/* temp/.* . 2>/dev/null && rm -rf temp
find . -type f ( -name “.astro” -o -name “.yml” -o -name “*.ts” )
-exec sed -i ‘’ “s/舊網域/$domain/g” {} +
npm install echo “✅ 專案 $name 已就緒”
### 11.2 品牌置換變數表 {#template-brands}
| 變數 | 模板預設值 | 新客戶 |
|------|----------|--------|
| 站名 | 銘的旅誌 | `___` |
| 網域 | travel.minglab.tw | `___` |
| 主色 | #E27D60 | `___` |
| 字體 | Inter + Noto Sans TC | `___` |
| Logo 文字 | 「銘的旅誌」 | `___` |
| 聯絡信箱 | info@你的網域 | `___` |
| 三大卡片 | 實用攻略 / 花費分享 / 行前準備 | `___` |
| Hero 標題 | 「用腳步丈量世界」 | `___` |
### 11.3 可複製 vs 需重做 {#template-copyable}
| 層級 | 可複製? |
|------|:--:|
| 架構(Astro + Sveltia + Cloudflare) | ✅ 100% |
| Layout / Header / Footer | ✅ 90% |
| config.yml | ✅ 80% |
| 部署流程 | ✅ 100% |
| GitHub Actions | ✅ 100% |
| OAuth Worker | ✅ 100% |
| 內容 .md | ❌ 0% |
| 照片 | ❌ 0% |
### 11.4 新專案時間線 {#template-timeline}
附錄 B:常用指令速查
| 建立 repo + push | 30 分 | | Cloudflare 部署 + DNS | 20 分 | | 調 config.yml | 30 分 | | 調配色 / Logo | 20 分 | | 調內容範例 | 20 分 | | OAuth Worker | 15 分 | | 測試 | 30 分 | | 客戶教學 | 1 小時 | | 總計 | 3-5 小時 |
[[#table-of-contents|← 回目錄]]
附錄 A:完整檔案對照表
| 檔案 | 用途 |
|---|---|
astro.config.mjs | Astro 設定(sitemap、site URL) |
package.json | 依賴 + scripts |
.gitignore | 排除 node_modules、dist |
AGENTS.md | OpenCode 提示 |
PROJECT_NOTES.md | 專案進度記錄 |
src/layouts/Layout.astro | 全域 HTML |
src/components/Header.astro | 導航列(ARIA) |
src/components/Footer.astro | 頁腳 |
src/components/TagList.astro | 標籤元件 |
src/pages/index.astro | 首頁 |
src/pages/blog/index.astro | 部落格列表 |
src/pages/blog/[...slug].astro | 文章 detail |
src/pages/destinations.astro | 目的地列表 |
src/pages/destinations/[slug].astro | 目的地 detail |
src/pages/gallery.astro | 攝影集 + Lightbox |
src/pages/about.astro | 關於我 |
src/pages/contact.astro | 聯絡表單 |
src/pages/admin-guide.astro | 後台使用說明 |
src/pages/404.astro | 404 |
src/pages/rss.xml.ts | RSS Feed |
src/content.config.ts | Content schema |
src/styles/global.css | 全域樣式 |
public/admin/config.yml | Sveltia CMS 設定 |
public/admin/index.html | CMS 入口 |
public/robots.txt | 爬蟲規則 |
public/og-default.svg | OG 圖片 |
oauth-worker/index.js | OAuth Worker |
oauth-worker/wrangler.toml | Worker 部署設定 |
.github/workflows/convert-webp.yml | 自動轉 WebP |
[[#table-of-contents|← 回目錄]]
附錄 C:網站風格選擇指南
附錄 B:常用指令速查
安裝(一次性,👤)
brew install git node lychee # 三條必裝工具
chmod +x audit.sh # 確保審計腳本可執行(每個新專案跑一次)
開發(👤 + 🤖)
npm run dev # 🤖→你:一開始由 AI 跑,確認後你接手驗收 → http://localhost:4321
npm run build # 🤖:AI push 前自動跑,確認 0 error。你也跑一次雙重確認
npm run preview # 👤:預覽靜態輸出,確認跟 dev 一致
Git(👤)
git add -A && git commit -m "訊息" && git push # 👤:你決定何時 push(觸發 Cloudflare 部署)
git pull --rebase # 👤:push 被拒時先拉再推(遠端有 CMS 內容更新)
ssh -T git@github.com # 👤:確認 SSH 連線正常(部署前置)
git remote -v # 🤖:AI 檢查是否推錯帳號/repo
檢查(🤖 跑,👤 看報告)
./audit.sh # 🤖:5 階段全檢查,你只看最後結果
./audit.sh https://新網域 # 🤖:指定網域的版本
lychee dist/ --base https://網域 --exclude "mailto:*" # 🤖:單跑 dead link(audit 第 1 階段)
手動抽查(👤)
curl -sL https://網域/ | grep -o '<title>[^<]*' # 👤:確認頁面標題不是「無標題」或舊站名
curl -sL https://網域/sitemap.xml | head # 👤:確認 sitemap 存在 + 網域不是 github.io
curl -sL https://網域/rss.xml | head -20 # 👤:確認 RSS 有產出
grep -rn "localhost" dist/ | grep -v "wrangler.json" # 👤:確認無 localhost 殘留在 HTML 中
grep -rn "github.io" dist/ | grep -v "wrangler.json" # 👤:確認無舊 GitHub Pages URL
grep "repo:" public/admin/config.yml # 👤:確認 CMS 推對 repo(跟 git remote -v 一致)
[[#table-of-contents|← 回目錄]]
附錄 C:網站風格選擇指南
C.1 為什麼風格要事先決定
風格決定 OpenCode 的初始提示詞範圍。先選定風格可以避免做到一半才改色。
| 影響範圍 | 說明 |
|---|---|
| 配色 | 主色、輔色、背景色、暗色背景 |
| 字體 | Google Fonts 載入順序 |
| 圓角 | Tailwind rounded class |
| 陰影 | 立體感風格 |
| 裝飾 | blur 圓圈、漸層、分隔線 |
C.2 8 種風格完整對照
詳細風格內容見下方子章節。
風格 1:日式侘寂風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 日式、禪意、侘寂、低飽和、天然材質 |
| 主色 | #8B7E74(灰褐) |
| 輔色 | #C9B99A(米色) |
| 背景 | 亮 #F5F0EB / 暗 #2C241B |
| 字體 | Noto Serif TC + Inter |
| 圓角 | rounded-lg |
| 陰影 | 無,用淡邊框 |
| 氛圍 | 極簡、禪意、大量留白 |
| 最適合 | 茶道、書法、器物、攝影 |
風格 2:海洋清新風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 海洋、清新、藍色、現代、涼爽 |
| 主色 | #0EA5E9(天藍) |
| 輔色 | #38BDF8、#0284C7 |
| 背景 | 亮 #F8FAFC / 暗 #0F172A |
| 字體 | Inter + Noto Sans TC |
| 圓角 | rounded-xl |
| 陰影 | 柔和藍色 shadow-lg |
| 氛圍 | 清涼、科技、專業 |
| 最適合 | 潛水、海洋生態、水上活動 |
風格 3:極簡黑白風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 極簡、黑白、粗框、瑞士設計 |
| 主色 | #1A1A1A(純黑) |
| 輔色 | #666666、#E5E5E5 |
| 背景 | 亮 #FFFFFF / 暗 #000000 |
| 字體 | DM Sans + Noto Sans TC |
| 圓角 | rounded-none |
| 陰影 | 無,用 border-2 黑框 |
| 氛圍 | 設計感、大膽 |
| 最適合 | 設計工作室、建築師、藝術家 |
風格 4:粉嫩柔和風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 粉彩、糖果色、柔和、可愛 |
| 主色 | #F472B6(粉紅) |
| 輔色 | #C084FC、#FB923C |
| 背景 | 亮 #FFF1F2 / 暗 #2D1B2E |
| 字體 | Quicksand + Noto Sans TC |
| 圓角 | rounded-3xl |
| 陰影 | 柔和粉色 |
| 氛圍 | 少女、溫暖、甜美 |
| 最適合 | 甜點店、手作、美容 |
風格 5:森林自然風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 森林綠、大地色、有機、環保 |
| 主色 | #166534(森林綠) |
| 輔色 | #22C55E、#713F12 |
| 背景 | 亮 #F7F6F3 / 暗 #1A2E1A |
| 字體 | Lora + Noto Sans TC |
| 圓角 | rounded-xl |
| 陰影 | 柔和綠色 |
| 氛圍 | 沉穩、環保、有機 |
| 最適合 | 露營、登山、農場 |
風格 6:日落暖橙色(目前風格)
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 暖橘、夕陽、旅行、日系文青 |
| 主色 | #E27D60(暖橘) |
| 輔色 | #E8A87C、#fcd34d |
| 背景 | 亮 #FDFBF7 / 暗 #1C1917 |
| 字體 | Inter + Noto Sans TC |
| 圓角 | rounded-2xl |
| 陰影 | 柔和暖色 shadow-xl |
| 裝飾 | 暖色 blur 圓圈 |
| 氛圍 | 溫暖、旅行、手帳感 |
| 最適合 | 旅遊、生活風格、咖啡店 |
風格 7:城市現代風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 都市、冷色、現代、線條、專業 |
| 主色 | #3B82F6(藍色) |
| 輔色 | #6366F1、#1E293B |
| 背景 | 亮 #FFFFFF / 暗 #0F172A |
| 字體 | Inter + Noto Sans TC |
| 圓角 | rounded-lg |
| 陰影 | 硬邊 shadow-lg |
| 氛圍 | 專業、現代、城市感 |
| 最適合 | 科技公司、SaaS、商務 |
風格 8:大膽撞色風
| 屬性 | 值 |
|---|---|
| 適合提示詞 | 撞色、大膽、藝術、霓虹、年輕 |
| 主色 | #FF3366(亮粉紅) |
| 輔色 | #FFD700、#00D2FF |
| 背景 | 亮 #FFFBEB / 暗 #1A1025 |
| 字體 | Space Grotesk + Noto Sans TC |
| 圓角 | rounded-2xl |
| 陰影 | 彩色強 shadow-xl |
| 氛圍 | 大膽、年輕、藝術 |
| 最適合 | 音樂節、潮流品牌、創意 |
8 種風格速查總表
| # | 風格 | 主色 | 最適合客戶 |
|---|---|---|---|
| 1 | 🌿 日式侘寂 | #8B7E74 | 茶道、器物、攝影 |
| 2 | 🌊 海洋清新 | #0EA5E9 | 潛水、海洋、水上 |
| 3 | 🖤 極簡黑白 | #1A1A1A | 設計師、建築師 |
| 4 | 🌸 粉嫩柔和 | #F472B6 | 甜點、手作、美容 |
| 5 | 🌲 森林自然 | #166534 | 露營、登山、農場 |
| 6 | 🌅 日落暖橙 | #E27D60 | 旅遊、生活、咖啡 |
| 7 | 🏙️ 城市現代 | #3B82F6 | 科技、SaaS、商務 |
| 8 | 🎨 大膽撞色 | #FF3366 | 音樂、潮流、創意 |
C.3 風格切換提示詞模板
萬用模板:
我要把網站改成 [風格名稱]。
關鍵詞:[詞1]、[詞2]、[詞3]
配色:主色[色碼] 輔色[色碼] 背景[色碼] 暗背景[色碼]
字體:標題[字體] 內文[字體]
圓角:[rounded-xx]
陰影:[無/shadow-md/lg/xl]
請先改 global.css + Layout.astro 讓我確認方向。
輕量模板(只改配色):
幫我把配色從暖色系改成 [色碼] 為主。只改顏色不改變局。先從 global.css、Layout.astro、index.astro 開始。
暗色模式獨立調整:
暗色模式配色跟新風格不搭。請獨立調整 dark: 相關 class,背景改[色碼] 卡片改[色碼] 文字改[色碼]。
C.4 設計參考資源
| 資源 | 網址 | 用途 |
|---|---|---|
| Dribbble | dribbble.com | 搜風格關鍵詞找靈感 |
| Behance | behance.net | 完整品牌案例 |
| Awwwards | awwwards.com | 頂尖網站設計 |
| Coolors | coolors.co | 快速生成配色 |
| Adobe Color | color.adobe.com | 從圖片提取配色 |
| Happy Hues | happyhues.co | 配色靈感 + 實際範例 |
C.5 換風格常見陷阱
| # | 陷阱 | 避免方式 |
|---|---|---|
| 1 | 改到功能 | Lightbox、Breadcrumb、ARIA 不要動 |
| 2 | 暗色模式漏改 | 每個亮色 class 確認 dark: 對應 |
| 3 | prose 樣式遺漏 | blog/destinations 獨立檢查 |
| 4 | 圖片卡片顏色斷層 | gallery 分類標籤色、目的地主題色要同步 |
| 5 | 全站一次改太多 | 先 global.css → 確認 → 再改頁面 |
| 6 | 色碼寫死 | 優先用 Tailwind 內建色碼 |
| 7 | 字體載入過重 | 不超過 3 個 Google Font 家族 |
| 8 | 裝飾過頭 | 過多 blur + 漸層降低效能 |
| 9 | transition 時間不一致 | 部分卡片用 300ms、部分用 1200ms → 視覺跳動不協調。全站統一 duration-[1200ms] |
附錄 D:客戶工具包
| 12 | 技術名洩漏到行銷頁 | 客戶看到「Home Assistant」「Zigbee」「Intel N100」會去比價。行銷頁用品牌化命名(AiHub C1),部落格保留 SEO 關鍵字。做完用 grep -rn "關鍵字" src/pages/ --include="*.astro" \| grep -v blog 檢查 |
| 13 | 聯絡頁格式反覆重構 | 三卡格式有三種:橫排小卡、直排小卡、正方卡。直接參考模板的 travel blog contact.astro 格式,不要從零設計 |
| 14 | 光暈風格不統一 | rounded-full(亮)vs rounded-md(淡)vs 有/無 dark:shadow。全站 badge 統一用 rounded-md + dark:shadow-[0_0_15px_rgba(255,255,255,0.1)] |
| 15 | 建完之後補齊 hover + blur | 所有頁面建完後,檢查 3 件事:① 所有卡片有 hover 效果嗎?② 所有區塊有 blur 背景嗎?③ 所有 badge 格式統一嗎? |
| 16 | global.css !important 全域 transition | 不要用 *, *::before, *::after { transition-duration: Xms !important; } — 會讓所有 HTML 的 duration-[1200ms] class 失效,除錯極難 |
| 17 | 暗色模式連結不明顯 | 瀏覽器預設藍色連結在暗色背景下難以辨識(深藍字 + 深灰底 = 看不到)。改用 site accent color:<a class="text-[主色] dark:text-[輔色] hover:underline">。兩站都碰到過,客戶說「連結點了沒反應」因為看不到 |
| 18 | 暗色模式 <hr> 分隔線刺眼 | 預設 <hr> 在暗色模式下是白色 / 淺灰色,極度刺眼。統一加 dark:border-gray-700。ming-website 踩坑後統一修正 |
C.6 全站 transition 統一規範(重要!)
問題: 在兩個真實專案(ming-travel-blog、ming-website)中,都發生了部分卡片使用 duration-300(0.3秒快速突兀)、部分使用 duration-[1200ms](1.2秒慢速自然),造成 hover 效果不一致、視覺上的微妙跳動感。
標準規範:
| 規則 | 值 | 說明 |
|---|---|---|
| 所有卡片/區塊 hover | transition-all duration-[1200ms] | 1.2 秒慢速自然飄浮 |
| 禁止 | duration-300 / 裸 transition-all | 前者太快、後者缺時間 |
| 全站統一 | 所有 .astro 頁面 + 元件 | 不分新舊,一律 1200ms |
修正方式(兩個專案經驗):
# 步驟 1:全站 duration-300 → duration-[1200ms]
for f in $(grep -rl "duration-300" src/pages/); do
sed -i '' 's/duration-300/duration-[1200ms]/g' "$f"
done
# 步驟 2:裸 transition-all → 補上 1200ms
for f in $(find src/pages src/layouts src/components -name "*.astro"); do
sed -i '' 's/transition-all\([^ ]\)/transition-all duration-[1200ms]\1/g' "$f"
done
# 步驟 3:檢查雙重
grep -c "duration-\[1200ms\] duration-\[1200ms\]" src/pages/*.astro
# 步驟 4:確認零 300ms
grep -c "duration-300" src/pages/**/*.astro
為什麼 1200ms: 首頁卡片(如「本地端中樞」「深度自動化」)從專案初期就使用 1200ms,這是視覺基準。後續新增的區塊若使用其他時間,hover 時會顯得「跳太快」。統一後,全站所有互動回饋節奏一致,體驗感提升明顯。
新專案注意: 每次 OpenCode 新增卡片區塊時,可能預設用 duration-300。完成所有頁面後,務必執行上述 4 步驟,確保全站一致。同時跑段落 D 的完整審計腳本(見 [[#pre-work-spec|§0.5 前置作業規範 → 段落 D]])檢查 shadow 公式、!important、技術名等。
[[#table-of-contents|← 回目錄]]
附錄 D:客戶工具包
D.1 客戶類型 × 網站場景對照
| 客戶類型 | 代表 | 核心需求 | 頁面數 | 已做案例 |
|---|---|---|---|---|
| 個人品牌 / 創作者 | 攝影師、部落客、設計師、作家 | 作品展示、文章發布、社群連結 | 10-15 | ming-travel |
| 微型科技企業 | 智慧家庭、IoT、SaaS、硬體新創 | 產品對比表、技術文章、品牌信任 | 10-14 | ming-website |
| 地方服務業 | 民宿、餐廳、咖啡廳、SPA、工作室 | 服務菜單、線上預約、Google 地圖 | 6-10 | — |
| 專業服務者 | 律師、會計師、教練、心理師、顧問 | 專業形象、預約諮詢、客戶案例 | 6-8 | — |
D.2 客戶需求面談問卷
第一次見面給客戶填(Google 表單或紙本),15 分鐘填完:
1. 網站的主要用途是什麼?
☐ 增加曝光(讓人在 Google 找到我)
☐ 展示作品 / 服務項目
☐ 建立專業形象(名片式網站)
☐ 賣東西(電商,需串接第三方)
2. 您的目標客群一句話描述?
(例:「25-40 歲、喜歡自助旅行的上班族」)
3. 希望訪客看完網站後採取什麼行動?
☐ 填聯絡表單 ☐ 打電話 ☐ 加 LINE ☐ 直接購買 ☐ 預約諮詢
4. 上線後誰會更新內容?
☐ 我自己 ☐ 公司小編 ☐ 沒人會更新(建立後就不動)
5. 如果自己更新,頻率大約是?
☐ 每週 1-2 篇 ☐ 每月 1-2 篇 ☐ 偶爾更新 ☐ 幾乎不更新
6. 提供 3 個你覺得「好看」的網站(同業或非同業皆可):
① __________________
② __________________
③ __________________
7. 提供 1-2 個你覺得「不好看」的網站,簡述原因:
______________________________
D.3 報價結構參考
以下為台灣個人接案 / 小型工作室市場行情(2026 年參考):
一次性建置費
| 方案 | 頁面 | 含後台 CMS | 預估工時 | 市場行情 |
|---|---|---|---|---|
| 輕量形象站 | 5-8 | — | 2-3 天 | NT$ 15,000-30,000 |
| 個人品牌部落格 | 10-15 | ✅ | 3-5 天 | NT$ 25,000-50,000 |
| 企業形象 + 產品 | 12-18 | ✅ | 5-8 天 | NT$ 40,000-80,000 |
| 複雜專案 | 20+ | ✅ | 2-4 週 | NT$ 80,000-150,000 |
月維護方案
| 方案 | 服務內容 | 行情 |
|---|---|---|
| 基礎維護 | 確保網站上線、後台可用、部署正常 | NT$ 1,500-3,000 /月 |
| 內容管理 | 基礎維護 + 每月代發 2 篇、換 5 張照片 | NT$ 3,000-5,000 /月 |
| 成長方案 | 內容管理 + 每月 SEO 報告 + 架構微調 | NT$ 5,000-8,000 /月 |
收費結構建議
| 階段 | 比例 | 時機 |
|---|---|---|
| 簽約金 | 30-50% | 開始設計前 |
| 尾款 | 50-70% | 上線後 3 天內付清 |
| 月維護 | 月付 | 每月 5 號前 |
客戶特別要求加價參考
| 項目 | 加價 |
|---|---|
| 急件(3 天內) | +50% |
| 多語系(英文版) | +60-100% |
| 自訂插畫 / 圖標 | 實報實銷 |
| 專業攝影 | 轉介攝影師 |
D.4 合約關鍵條款
以下為建議,正式合約請諮詢律師。
A. 交付範圍
- 明確列出:頁面數量、collection 數量、功能細項
- 明確排除:內容填寫、照片拍攝、SEO 排名保證、新功能開發
B. 付款條件
- 簽約後 3 日內付簽約金 40%
- 網站上線後 3 日內付尾款 60%
- 逾期:每逾一日加收總價 0.1% 滯納金(最高 20%)
C. 保固範圍(30 天)
- 包含:部署異常、後台無法登入、頁面顯示錯誤
- 不包含:新功能開發、風格變更、內容更新、SEO 排名、第三方服務故障
D. 智慧財產權
- 網站程式碼:歸開發者所有,客戶取得永久使用授權
- 客戶提供的內容(文字、照片、商標):歸客戶所有
- 開放原始碼元件(Astro、Tailwind、Sveltia CMS):依各別授權條款
E. 終止條件
- 任一方未履行義務,書面通知 14 日未改善,可終止合約
- 終止時已完成工作按比例計價、未完成部分不另收費
D.5 客戶常問 FAQ(17 題)
| # | 客戶問 | 回答 |
|---|---|---|
| 1 | 多少錢? | 看頁數,5-8 頁形象站約 2-3 萬,含後台部落格約 3.5-5 萬。確定需求後出正式報價單 |
| 2 | 多久做好? | 簽約後 3-5 個工作天交付第一版 |
| 3 | 可以自己改內容嗎? | 可以,登入後台就能發文、換照片、改文字,不需要寫程式 |
| 4 | Google 搜得到嗎? | 架構已完成 SEO(sitemap、JSON-LD、meta),教你送 Search Console 後 1-2 週開始出現 |
附錄 E:自動化驗證腳本
| 7 | 可以做購物車嗎? | 可以串接第三方金流(綠界、Shopify),但建議有穩定訂單量再做 |
| 8 | 網站會被攻擊嗎? | 純靜態網站無資料庫,沒有 SQL injection 的入侵入口。CDN 自帶 DDoS 防護 |
| 9 | 跟你做跟找設計公司差在哪? | 快(5 天 vs 1 個月)、便宜(約 1/3 價格)、零月費、你自己能更新內容 |
| 10 | 以後換人做,網站可以拿走嗎? | 原始碼在 GitHub Private Repo,你可以隨時交接給下一位工程師 |
| 11 | FB 上 500 元就有人做,為什麼價差這麼大? | 500 元套版:無法自己改內容、手機版排版會暴走、Google 搜不到、沒有後台。你拿到的是:自己改內容的後台 + 手機完美 + Google 能搜 + 無月費 + 保固 |
| 12 | 做好了,怎麼讓更多人來看? | 三步驟:① 送 Search Console(我教你)② 網址放到 IG/LINE/FB 簡介 ③ 每週發一篇文。持續 3 個月就有穩定流量 |
| 13 | 我的聯絡表單真的會寄到嗎? | mailto 在手機上如果沒裝郵件軟體會失敗。建議加裝 Cloudflare Email Routing(免費),讓 info@你的網域 自動轉到你的 Gmail |
| 14 | 後台會被駭客攻擊嗎? | Git OAuth 登入 = 最高安全等級。可以在 Cloudflare WAF 加規則:後台路徑 /admin/* 限定台灣 IP,阻擋海外暴力嘗試 |
| 15 | 有人抄我的網站怎麼辦? | GitHub Private Repo → 原始碼不外洩。前端 HTML/CSS 瀏覽器一定看得到,但後台邏輯、內容管理系統是隱藏的 |
| 16 | 信箱要錢嗎?要不要另外買? | 不用。Cloudflare Email Routing 免費提供 info@你的網域,自動轉到你的 Gmail。專業信箱零月費 |
| 17 | 為什麼你的方案不用月費,其他公司都要收? | 傳統架站靠主機費/信箱費/維護費賺月費。我們用 Cloudflare 免費方案 + 靜態網站技術,全自動化。客戶只付一次建置費 + 每年網域費 NT$300-800 |
D.6 接案策略與收入評估
| 客戶類型 | 客源 | 單價 | 月接件數 | 月營收範圍 |
|---|---|---|---|---|
| 個人品牌 / 創作者 | FB 社團、IG、熟人轉介 | 2.5-5 萬 | 2-3 件 | 5-15 萬 |
| 微型科技企業 | 創業社群、Meet Taipei、朋友介紹 | 4-8 萬 | 1-2 件 | 4-16 萬 |
| 地方服務業 | Google 地圖、地方社群 | 1.5-3 萬 | 3-4 件 | 4.5-12 萬 |
| 專業服務者 | LinkedIn、產業聚會 | 3-6 萬 | 1-2 件 | 3-12 萬 |
關鍵是兩個累積型數字,不是月營收:
| 累積資產 | 說明 |
|---|---|
| 月維護費 | 每個案子 1,500-3,000/月,累積 10 個客戶 = 每月 1.5-3 萬被動收入 |
| 轉介率 | 客戶滿意 → 推薦朋友 → 零廣告費取得新客戶 |
可延伸服務金字塔
你現有能力可立刻包裝的加值服務:
| 層級 | 服務 | 定價 | 工時 | 說明 |
|---|---|---|---|---|
| 1 | Content 代寫 | NT$500-1,500/篇 | 30 分 | AI 生成 + 你潤稿 |
| 2 | 每季 SEO 報告 | NT$1,500-3,000/次 | 30 分 | Search Console + GA4 截圖,AI 產報告 |
| 3 | Google 商家代辦 | NT$1,500-3,000 | 20 分 | 申請 + 驗證 + 照片上傳 |
| 4 | LINE 官方帳號串接 | NT$1,000-2,000 | 15 分 | 網站加 LINE 按鈕,後台設 LINE ID 欄位 |
| 5 | 網站速度健檢 | NT$1,000/次 | 15 分 | Lighthouse 報告 + 建議 |
策略:基礎建置不賺大錢,靠這些輕量加值服務疊高每客戶營收。全部做完 5 項,每客戶額外營收 NT$5,000-11,500。
[[#table-of-contents|← 回目錄]]
附錄 E:自動化驗證腳本
交付給客戶前,跑完這四個腳本。一個指令 = 一份驗收報告。
E.1 dead link 檢查
# 安裝 lychee(一次性)
brew install lychee
# build 後掃描
npm run build
lychee dist/ --base https://你的網域 --no-progress --exclude "mailto:*"
綠色 = 正常,紅色 = 斷裂連結,要修。
E.2 Sitemap 驗證
DOMAIN="https://你的網域"
# 確認 sitemap-index 使用正確網域(非 localhost 或舊域名)
curl -s "$DOMAIN/sitemap-index.xml" | grep "$DOMAIN"
# 逐一檢查內頁 sitemap 可否訪問
curl -s "$DOMAIN/sitemap-index.xml" | grep -oP '(?<=<loc>)[^<]+' | while read url; do
code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
if [ "$code" != "200" ]; then
echo "❌ $code $url"
else
echo "✅ $code $url"
fi
done
E.3 全頁面 curl 檢查
DOMAIN="https://你的網域"
# 從 src/pages/ 掃出所有頁面路徑,逐一 curl
for f in $(find src/pages -name "*.astro" -not -path "*/admin/*" | sort); do
path=$(echo "$f" | sed -E \
-e 's|src/pages||' \
-e 's|/index\.astro$|/|' \
-e 's|\.astro$|/|' \
-e 's|\[\.\.\.slug\]||' \
-e 's|\[slug\]|test-slug|')
[ "$path" = "/" ] && path="/"
code=$(curl -s -o /dev/null -w "%{http_code}" "${DOMAIN}${path}")
if [ "$code" != "200" ]; then
echo "❌ $code ${DOMAIN}${path}"
else
echo "✅ $code ${DOMAIN}${path}"
fi
done
E.4 一鍵審計(交付前最終檢查)
#!/bin/bash
# audit.sh — 交付客戶前最後檢查,一個指令跑完
DOMAIN="${1:-https://你的網域}"
PASS=0
FAIL=0
echo "=== 1/5 dead link 檢查 ==="
lychee dist/ --base "$DOMAIN" --no-progress --exclude "mailto:*" 2>/dev/null
[ $? -eq 0 ] && ((PASS++)) || ((FAIL++))
echo ""
echo "=== 2/5 Sitemap 網域檢查 ==="
curl -s "$DOMAIN/sitemap-index.xml" | grep -q "$DOMAIN"
[ $? -eq 0 ] && echo "✅ 網域正確" && ((PASS++)) || { echo "❌ Sitemap 網域錯誤"; ((FAIL++)); }
echo ""
echo "=== 3/5 dist/ 無 localhost 殘留 ==="
grep -rn "localhost" dist/ 2>/dev/null
[ $? -ne 0 ] && echo "✅ 無 localhost 殘留" && ((PASS++)) || { echo "❌ 發現 localhost 殘留"; ((FAIL++)); }
echo ""
echo "=== 4/5 dist/ 無舊 GitHub Pages 殘留 ==="
grep -rn "github\.io\|githubpages" dist/ 2>/dev/null
[ $? -ne 0 ] && echo "✅ 無舊 GitHub Pages 殘留" && ((PASS++)) || { echo "❌ 發現舊 GitHub Pages 殘留"; ((FAIL++)); }
echo ""
echo "=== 5/5 全頁面 200 檢查 ==="
for f in $(find src/pages -name "*.astro" -not -path "*/admin/*" | sort); do
path=$(echo "$f" | sed -E -e 's|src/pages||' -e 's|/index\.astro$|/|' -e 's|\.astro$|/|' -e 's|\[\.\.\.slug\]||' -e 's|\[slug\]|test-slug|')
[ "$path" = "/" ] && path="/"
code=$(curl -s -o /dev/null -w "%{http_code}" "${DOMAIN}${path}")
[ "$code" = "200" ] && echo "✅ $code ${DOMAIN}${path}" || { echo "❌ $code ${DOMAIN}${path}"; ((FAIL++)); }
done
echo ""
echo "=== 結果 ==="
echo "通過: $PASS / 5"
echo "失敗: $FAIL"
[ $FAIL -eq 0 ] && echo "🎉 全部通過,可以交付客戶!"
交付客戶時,執行 ./audit.sh https://客戶網域,把輸出截圖附在驗收單裡。
audit.sh已存在於兩個參考專案的根目錄(ming-travel-blog/audit.sh、ming-website/audit.sh)。新專案可直接複製,只改 DOMAIN 變數即可。
E.5 WAF 安全防護(選配)
如果客戶擔心後台被攻擊,可以加一條 WAF 規則:
① Cloudflare 面板 → 你的網域 → Security → WAF
② Create Rule → Field: URI Path → Operator: contains → Value: /admin/
③ AND Field: Country → Operator: does not equal → Value: Taiwan
④ Action: Block
⑤ 完成。海外 IP 無法存取後台路徑
Security Headers(選配,貼到 astro.config.mjs):
// astro.config.mjs 加入 custom headers
import { defineConfig } from 'astro/config';
export default defineConfig({
// ...其他設定
server: {
headers: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
}
}
});
[[#table-of-contents|← 回目錄]]
SOP 手冊 v3.0 — 基於 ming-travel-blog + ming-website 專案真實開發經驗 最後更新:2026-05-01