Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🚀 靜態網站建置 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%)

結果:零伺服器、零資料庫、零月費、零維護、永遠不會被駭

跟其他方案比

我們的方案WordPressWix / Squarespace找人客製
建置費一次性一次性
月費$0$5-25$16-40$0
主機費$0$5-30/月內含$5-30/月
載入速度< 1 秒2-5 秒2-8 秒看品質
SEO 評分95+70-9060-80看品質
安全性無法被駭需定期更新平台負責需維護
備份GitHub 永久需外掛平台負責需自己來
搬家一鍵搬走可匯出困難困難
後台表單式直覺功能多但複雜拖拉式不一定
照片管理自動壓縮 90%需外掛自動不一定
專業信箱✅ 免費NT$200-300/月❌ 自費加購NT$200-300/月
監控告警✅ 免費需外掛平台內建需自行
自動化驗證✅ 一鍵 audit.sh
手機版自動適應(含上傳壓縮)看主題看品質
原始碼客戶擁有
被平台綁架完全沒有有一點嚴重沒有

14 大優勢

#優勢對客戶的意義
1永久零月費網站做好後永遠不用再付錢,唯一成本是網域年費
2全球極速Cloudflare CDN 330+ 城市有節點,載入 < 1 秒
3無法被駭無資料庫 = 無 SQL injection,無後端 = 無伺服器入侵
4SEO 天生高分純 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,000NT$ 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 同步
Lighthouse95+🟢靜態網站天生高分
圖片空間4 張 / 1.9 MB🟢Actions 自動轉 WebP
程式碼重複0🟢無冗餘
閒置檔案0🟢已清理乾淨

這是商業模版嗎?

是,而且是最理想的商業模版。 只需幾個步驟就可複製給新客戶:

要改的時間
config.yml — 調整 collection 欄位30 分
.astro 頁面 — 改配色、Logo 文字20 分
astro.config.mjs — 改 site URL1 分
內容 .md — 清空,填客戶自己的20 分

從零到上線:3-5 小時。

客戶溝通金句

「你的網站跟 Google、Facebook 跑在同一條高速公路上。」
「沒有伺服器 = 沒有月費 = 不會被駭。」
「三年後你只會付網域費,網站還是一樣快。」
「你寫文章,其他全部自動化。」
「這不是省錢,這是把錢花在對的地方。」

[[#table-of-contents|← 回目錄]]


第 1 章:準備工具與環境

👤 你:安裝軟體、註冊帳號🤖 AI:確認環境就緒、產出費用總表

1.1 需要安裝的軟體

#工具用途何時用安裝必裝
1Node.js >= 22執行 Astro、npm run dev/build開發全程nodejs.org
2Git版本控管、push 觸發部署開發全程brew install git
3HomebrewmacOS 套件管理(裝其他工具的前提)安裝工具時單次brew.sh
4OpenCodeAI 協作:寫碼、審計、除錯、文件產出開發全程opencode CLI
5lychee全站 dead link 掃描(audit.sh 第 1 階段)交付前跑一次brew install lychee
6curlHTTP 測試:檢查頁面回 200、sitemap 正確、RSS 正常開發 + 交付macOS 內建
7grep文字掃描:搜尋 localhost 殘留、舊部署網址、技術名洩漏開發 + 交付macOS 內建
8digDNS 查詢:檢查網域 A 記錄是否生效部署除錯macOS 內建
9sshGitHub 連線測試:ssh -T git@github.com部署前置macOS 內建
10ChromeLighthouse 效能測試(§9.4)交付前google.com/chrome
11UptimeRobot網站監控告警(掛了寄 Email 給你,§9.7)上線後永久uptimerobot.com
12ObsidianMarkdown 閱讀器,支援 Wiki Link 跳轉、搜尋讀手冊obsidian.md
13VS Code程式碼編輯器(選配,grep/curl 為主)進階除錯code.visualstudio.com

前 9 項是一條龍安裝:brew install git node lychee。其他 macOS 內建。

1.2 需要註冊的服務

服務用途網址
GitHub存放程式碼(Private repo)github.com
Cloudflare部署 + DNS + CDNdash.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🤖OpenCodeAI 建 AGENTS.md + 視覺規範表,你確認
3🤖OpenCodeAI 產出 .astro 頁面、config.yml、index.html
4👤npmnpm run dev本地預覽 http://localhost:4321,你驗收
5👤npmnpm run build確認 0 error 才能 push
6🤖grepgrep -rn "關鍵字" src/pages/AI 跑技術名防洩漏檢查

第三階段:部署後檢查(交付前)

工具指令做什麼
1🤖audit.sh./audit.shAI 一鍵跑 5 階段檢查
2🤖lychee(由 audit.sh 自動執行)AI 掃描全站 dead link
3👤curlcurl -sL https://網域/你確認頁面標題、sitemap 正確
4👤ChromeDevTools → 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 綁定,自動設 CNAMEPages 幫你處理 DNS,不用手動加 A 記錄
Cloudflare Worker(OAuth)用 Workers Routes,自動處理Worker 自己接 api/* 路徑
網域不在 Cloudflare 註冊去原註冊商把 nameserver 指向 CloudflareDNS 託管權要轉移到 Cloudflare
DNS 改了沒生效等 1-5 分鐘DNS 傳播有延遲,不是即時

最常見的 DNS 錯誤:A 記錄指向舊主機 IP 如果你在 Cloudflare DNS 面板看到 A 記錄 minglab.tw → 某個 IP,但網站是部署在 Pages 上 → 這兩者在打架 → 網站打不開。解法:刪掉 A 記錄,讓 Pages 的 Custom Domain 自己處理。

A.1 建立 Private Repo

  1. github.com/new
  2. Name:專案名稱
  3. Private
  4. 不要勾 Initialize with README
  5. 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 failedssh-keyscan github.com >> ~/.ssh/known_hosts
推到舊帳號確認 git remote -v 的 URL 是 git@github.com:正確帳號/正確repo.git
兩個帳號的 SSH Key 衝突Git 預設只會用 id_ed25519。多帳號需要在 ~/.ssh/configHost 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 existsgit 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 建立

步驟操作
1GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
2Application name:專案名稱-cms
3Homepage URL:https://你的網域
4Authorization callback URL:https://你的網域/api/callback只放一個
5Register → 複製 Client ID + Client Secret

每個網站要有獨立的 OAuth App,不要共用。

陷阱解法
多個 callback URLGitHub 可能選錯,只用一個專屬該網域的 URL
Client Secret 洩漏只放進 oauth-worker/index.js,不要 commit 到 repo

B.7 建立 Token 登入(客戶第一次使用)

這是客戶登入後台的方式。做一次,瀏覽器記住,之後不用再貼。

你幫客戶做的(一次性)

操作說明
1GitHub → 右上頭像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new tokengithub.com/settings/tokens/new
2Note: 客戶名-網站名以後知道這個 Token 是誰的
3Expiration: No expiration客戶不會半年後突然被鎖
4勾選 repo + workflow這兩個就夠了
5Generate → 立刻複製(離開頁面後看不到)貼到 1Password / KeePass / LINE 存檔給客戶

客戶第一次用

操作
1打開 https://網站/admin/
2Token 按鈕
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
DNSCustom 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
以上都不符合不用手動設任何東西

最常見的三個卡關點

#卡在哪原因解法
1Cloudflare 看不到 repoGitHub Applications 沒勾選新 repo回步驟 2
2Deploy 失敗(npx wrangler)Framework 被自動猜錯重新 Deploy → 手動設 Framework: None
3Custom Domain 綁不上去舊 DNS 記錄卡住刪掉舊記錄,等 1 分鐘再試

C.3 Build 設定

欄位⚠️
Framework presetNone← 關鍵!不要選 Astro
Build commandnpm run build
Build output directorydist
Deploy command(如有)留空

選 Astro 會強制跑 npx wrangler deploy → 需要 adapter → 失敗。

C.4 Connect GitHub → 自動部署

  1. Cloudflare → Workers & Pages → Create applicationContinue with GitHub
  2. 授權 → 選 repo → Begin setup
陷阱解法
Cloudflare Pages 找不到新 repo(最高頻率卡關)每開一個新 GitHub repo 都要來這裡:GitHub → 右上頭像 → Settings → Applications → Cloudflare Pages → Configure → Repository access → 搜尋新 repo → 勾選 → Save。沒勾 = Cloudflare 看不到 = 無法部署
點了 Continue with GitHub 直接跳到 Workersrepo 有舊 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 builddist/
2建 OAuth WorkerWorkers → Create → Start with Hello World!獨立 Worker,只處理 GitHub 登入
3貼程式碼Edit code → 貼 oauth-worker/index.js確認 Client ID + Secret 正確
4設 Routes網域/api/* → OAuth Worker / 網域/* → 主站api/ 必須在上面*
5Custom DomainDNS 自動處理輸入網域即可,Cloudflare 自動建 CNAME
6Custom Domain + HTTPSWorkers & Pages 設定等 1-2 分鐘

D.1 OAuth Worker 建立

  1. Workers & Pages → Create → Workers → 選 Start with Hello World!
  2. 名稱:專案名-oauth
  3. 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必須 SaveDeploySave and Deploy 兩個按鈕
Client ID 是舊的curl -sI https://網域/api/auth 檢查 client_id=

D.3 Workers Routes 設定

優先RouteWorker說明
1網域/api/*專案名-oauthOAuth 必須在上面
2網域/*專案名主站在下面
陷阱解法
api/* 指錯 Worker從另一個專案的 OAuth 複製時忘記改
api/* 放在 * 下面/api/callback 永遠被主站攔截 → OAuth 失敗

D.4 後台登入測試

測試網址預期
後台畫面https://網域/admin/Sveltia CMS 三個按鈕
OAuth 回呼https://網域/api/auth302 跳轉 GitHub 授權
Token 登入後台點 Token → 貼上 PAT進入 CMS 編輯頁面

D.5 Cloudflare API Token(給 OpenCode 代理用)

步驟操作
1dash.cloudflare.com/profile/api-tokensCreate Token
2Custom Token
Permissions
Account → Cloudflare PagesEdit
Account → Workers ScriptsEdit
User → User DetailsRead
  1. 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:緊急救援

部署後出問題,從上往下檢查,不要反過來:

順序檢查指令
DNSdig 網域 +short
RoutesCloudflare 面板 → Workers Routes
部署狀態Cloudflare → Deployments
config.ymlgrep "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
8hover 光暈公式___hover:shadow-[0_0_20px_rgba(主色,0.15)] dark:hover:shadow-[0_0_20px_rgba(主色,0.25)]
9transition 時間1200ms全站只用這一個值
10badge 光暈風格___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確保手機上兩邊等高
4CTA 按鈕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)及早發現不一致
5push 前跑全站審計確認 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
6AGENTS.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 buildbuild 不過立刻回報,不要累積問題
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 篇
/blogblog/index.astro動態部落格文章列表
/blog/[...slug]blog/[...slug].astro動態文章 detail
/destinationsdestinations.astro動態目的地卡片列表
/destinations/[slug]destinations/[slug].astro動態目的地 detail
/gallerygallery.astro動態攝影集 + Lightbox
/aboutabout.astro動態關於我(CMS collection)
/contactcontact.astro靜態聯絡表單(mailto)
/admin-guideadmin-guide.astro靜態後台使用說明
/404404.astro靜態404 頁面
/privacyprivacy.astro靜態隱私權政策
/termsterms.astro靜態使用條款
/rss.xmlrss.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 色下拉選單主題色
  • 封面圖可選(沒圖直接從標題開始)
  • <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: "夜拍、古都、沙漠..." }
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-12mt-16 py-8,間距減半
按鈕連結 404頁面刪除/改名後,檢查所有 href="/old-path" 是否還有效

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.ymlbuttonText 但 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.ymlrepo: 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.twCloudflare DNS → 加一條 A record @ → Proxy 開(橘雲)
子域名(如 travel.xxx.twCloudflare 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.ymlrepo 欄位名稱不對。

# 確認 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 → 再刪 KVSettings → Bindings → 移除 SESSION

7.16 頁面標題沒更新

Layout.astro 改了預設 title,但某些頁面 <Layout title="舊標題"> 硬編碼覆蓋了預設值。

檢查指令
所有頁面的 title propgrep -rn "Layout title=" src/pages/ --include="*.astro"
預設 titleLayout.astrotitle = '...'

真實案例: Layout.astro 改了「銘誠科動」但 index.astro 硬編碼「銘於心」,部署後標題一直沒變。

第 8 章:測試與交付檢查

如果選 OAuth 模式(§2 Part D),部署步驟照正確順序走(DNS → Build → Connect → Custom Domain),這一節你永遠不會用到。Pages 模式不需要 Routes,此節不適用。 這是兩個專案各花了 2 小時才學到的教訓。

部署後出問題,從上往下檢查,不要反過來:

順序檢查指令說明
DNSdig 網域 +short域有沒有指向 Cloudflare?
RoutesCloudflare 面板 → Workers Routesapi/* 指對 Worker 嗎?
部署狀態Cloudflare → Deployments最新部署成功嗎?
config.yml 名稱grep "repo:" public/admin/config.ymlrepo 名稱對嗎?
頁面硬編碼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 壞了的三種查法

方法指令 / 位置說明
看 LogCloudflare 面板 → Workers & Pages → oauth-worker → Logs看有沒有紅色錯誤,特別是 401 (token expired) 或 500
即時 tailnpx 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_SECRETCloudflare 面板 → oauth-worker → Settings → Variables
後台 /admin/ 404 Not FoundWorkers Routes 沒設定 api/* 指到 oauth-workerCloudflare 面板 → 你的網域 → 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/5dead link客戶點連結跳 404,覺得你網站壞了
2/5Sitemap 網域正確Google 收錄到舊網域,新站永遠搜不到
3/5dist/ 無 localhost 殘留網站出現 http://localhost 連結,客戶點了什麼都看不到
4/5dist/ 無舊 GitHub Pages 殘留舊部署網址遺留在網站上,客戶困惑
5/5全頁面回 200某頁掛了你不知道,客戶先發現

目標:5/5 全部通過才能交付。

audit.sh 常見錯誤解析

跑完 audit.sh 後,不用每個紅字都緊張。以下是判定標準:

錯誤例子判決說明
外部連結 404home-assistant.io/硬體/ 文件搬家✅ 可不修非本站控制,原作者改網址了
內部 sitemap 多出頁面/services/ 在 sitemap 但頁面已刪⚠️ 查後台有人在 CMS 發了又刪,從後台查
動態路由 test-slug 404destinations/test-slug/ 回 404✅ 可不修腳本用假 slug 測試,沒這篇文章正常
canonical URL 錯誤頁面 <link rel="canonical"> 寫著 github.io❌ 必須修Google 會收錄到舊網域,新站永遠搜不到
頁面不回 200/about/ 回 500❌ 必須修客戶點了會看到錯誤
localhost 殘留dist/ 裡有 wrangler.json"ip":"localhost"✅ 可不修Cloudflare adapter 開發設定,非真實 leak
外部連結 403raspberrypi.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 桌面版檢查

#檢查項目方法通過
1nav sticky捲動頁面,確認導航列固定在頂部
2對比表確認是 <table> 格式,非手機卡片
3hover 光暈0_0_20px_rgba(主色,0.15) + duration-[1200ms]
4所有 hover每個可點擊元素 hover 有過渡動畫
5暗色模式切換暗色模式,區塊背景正確(非黑色)
6圖片比例確認照片無變形

8.4 後台 CMS 檢查

#檢查項目說明通過
1OAuth 登入/api/auth → 302 導向 GitHub
2Token 登入用 Token 可正常進後台
3看得到內容後台左側選單顯示所有 collection
4新增文章新增一篇測試文 → 發布 → 前台出現
5編輯文章修改既有文章 → 發布 → 前台更新
6刪除文章刪除測試文 → 前台消失
7上傳圖片上傳一張圖片 → 出現在媒體庫
8navigationGuard點「新建/編輯」→ 輸入內容 → 按 F5 → 是否彈確認框

8.5 OAuth 檢查

#檢查指令通過
1Worker 有部署Cloudflare Workers & Pages → oauth-worker
2環境變數env.GITHUB_CLIENT_IDenv.GITHUB_CLIENT_SECRETenv.REDIRECT_URI
3回呼路徑REDIRECT_URI = https://你的網域/api/auth/callback
4Routes 路徑https://你的網域/api/* → oauth-worker

8.6 交付清單(給客戶前最後確認)

#項目通過
1網域可正常訪問
2後台網址可登入(https://網域/admin/
3把後台帳號/密碼存到客戶的 1Password 或 KeePass
4後台使用說明頁(/admin-guide)可訪問且正確
5聯絡表單寄得到客戶信箱
6OAuth 憑證屬於客戶(非共用)
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 image1200×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≥ 9095-100
Accessibility≥ 95100
Best Practices≥ 90100
SEO= 100100

常見扣分原因與對策:

扣分原因解法
圖片過大(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 縮寫 → 完整數字 → 心理定價 → 折抵策略)。

強制規則:

規則說明範例
永遠完整數字禁止 3K10K 等縮寫NT$ 3,000-10,000NT$ 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.mjsAstro 設定(sitemap、site URL)
package.json依賴 + scripts
.gitignore排除 node_modules、dist
AGENTS.mdOpenCode 提示
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.astro404
src/pages/rss.xml.tsRSS Feed
src/content.config.tsContent schema
src/styles/global.css全域樣式
public/admin/config.ymlSveltia CMS 設定
public/admin/index.htmlCMS 入口
public/robots.txt爬蟲規則
public/og-default.svgOG 圖片
oauth-worker/index.jsOAuth Worker
oauth-worker/wrangler.tomlWorker 部署設定
.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 設計參考資源

資源網址用途
Dribbbledribbble.com搜風格關鍵詞找靈感
Behancebehance.net完整品牌案例
Awwwardsawwwards.com頂尖網站設計
Coolorscoolors.co快速生成配色
Adobe Colorcolor.adobe.com從圖片提取配色
Happy Hueshappyhues.co配色靈感 + 實際範例

C.5 換風格常見陷阱

#陷阱避免方式
1改到功能Lightbox、Breadcrumb、ARIA 不要動
2暗色模式漏改每個亮色 class 確認 dark: 對應
3prose 樣式遺漏blog/destinations 獨立檢查
4圖片卡片顏色斷層gallery 分類標籤色、目的地主題色要同步
5全站一次改太多先 global.css → 確認 → 再改頁面
6色碼寫死優先用 Tailwind 內建色碼
7字體載入過重不超過 3 個 Google Font 家族
8裝飾過頭過多 blur + 漸層降低效能
9transition 時間不一致部分卡片用 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 效果不一致、視覺上的微妙跳動感。

標準規範:

規則說明
所有卡片/區塊 hovertransition-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-15ming-travel
微型科技企業智慧家庭、IoT、SaaS、硬體新創產品對比表、技術文章、品牌信任10-14ming-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-82-3 天NT$ 15,000-30,000
個人品牌部落格10-153-5 天NT$ 25,000-50,000
企業形象 + 產品12-185-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可以自己改內容嗎?可以,登入後台就能發文、換照片、改文字,不需要寫程式
4Google 搜得到嗎?架構已完成 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 萬被動收入
轉介率客戶滿意 → 推薦朋友 → 零廣告費取得新客戶

可延伸服務金字塔

你現有能力可立刻包裝的加值服務:

層級服務定價工時說明
1Content 代寫NT$500-1,500/篇30 分AI 生成 + 你潤稿
2每季 SEO 報告NT$1,500-3,000/次30 分Search Console + GA4 截圖,AI 產報告
3Google 商家代辦NT$1,500-3,00020 分申請 + 驗證 + 照片上傳
4LINE 官方帳號串接NT$1,000-2,00015 分網站加 LINE 按鈕,後台設 LINE ID 欄位
5網站速度健檢NT$1,000/次15 分Lighthouse 報告 + 建議

策略:基礎建置不賺大錢,靠這些輕量加值服務疊高每客戶營收。全部做完 5 項,每客戶額外營收 NT$5,000-11,500。

[[#table-of-contents|← 回目錄]]


附錄 E:自動化驗證腳本

交付給客戶前,跑完這四個腳本。一個指令 = 一份驗收報告。

# 安裝 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.shming-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