astro-github-pages — Astro + GitHub Pages 建站 SOP
從零到「Pages 上線、兩道守門生效、搜尋引擎收得到」。
不要自行發明頁面內容或資訊架構。 骨架上線後就去問使用者要網站地圖與發展方向。這條是刻意的:AI 自己編出來的頁面結構通常對不上真實業務。
模板檔在本 skill 目錄的 templates/。
為什麼要有守門
兩個問題只有在 CI 擋下來才會真的被解決:
- 設計會漂。 一個人寫
#3a7、一個人寫rgb(51,119,85)、下一個人寫var(--color-primary),三個月後沒人知道哪個是對的。字級同理,font-size: 14px一旦混進來就會繁殖。 - AI 寫的中文有指紋。 「不僅…更」「值得注意的是」「隨著…的發展」「這不是 X,而是 Y」。單獨看每一句都通順,整篇讀起來就是機器味。人工複核會累、會鬆,正則不會。
所以 build 一定串這兩支腳本,CI fail 就不上線。
守門規則
設計守門 scripts/check-design.mjs
掃 src/ 下所有 .css/.astro/.svelte,違規 exit 1:
- 禁 px 字級。一律
var(--text-*)階梯,最小 18px,內文不小於--text-base。 - 顏色只准出現在
src/styles/variables.css。oklch 為準、hex 作 fallback。 - 禁
!important。 - 禁外部 CDN(fonts.googleapis、gstatic、cdnjs、unpkg、jsdelivr)。字型用系統堆疊。
- css 檔白名單:
src/下的.css只准styles/variables.css與styles/global.css。新增 css 檔即 fail,元件樣式寫 scoped<style>。 - 階梯下限:
--text-*的 token 值本身一律不小於 18px(1.125rem),clamp()以最小值計。少了這條,前面第 1 條會被「把 token 值調小」繞過。
去 AI 味守門 scripts/check-content.mjs
掃 src/**/*.md(x),兩級判定:
- ERROR(單一命中即擋):誤判率接近零的強 AI 指紋。「不是 X 而是 Y」、「不僅…更」、「值得注意的是」、「換句話說」式的空泛收束、「隨著…的發展」開場、模糊引用(研究顯示/專家認為)、模板化的第一人稱開場。
- WARN(軟訊號):分詞彙、句式、結構、語氣四層。破折號、「這是一個 X,也是一個 Y」、拔高收尾、賦能/助力這類詞。單一命中不擋;同一檔跨三個以上不同層級才升級成 ERROR。
那條「跨三層才算」是重點。單一特徵人類也會用,只憑一個就擋會產生大量誤判,接著團隊就會開始忽略守門,守門就死了。
掃描範圍刻意只掃變動檔(相對 origin/main)。抓不到 git base 時掃 0 個檔並 exit 0,永不誤擋。這樣既有內容不必先全部改乾淨才能用,新內容從今天開始守住。另有 pnpm check:content:all 做全站普查,恆 exit 0,只報告不擋。
逐字轉錄豁免:frontmatter 標 sourceVerbatim: true 的檔整檔跳過。用途是「把既有網站原文一字不改搬過來」,原文出現「不僅…更」是原作者寫的,改寫它等於竄改別人的文案。只准用於逐字轉錄;新寫的文案掛這個旗標等於自廢守門,驗收時要逐檔比對旗標檔是否真的出自來源。
站台特化規則不進通用模板,各站在腳本內的擴充點自行增補。
Phase 0. 收集輸入(缺才問,一次問完)
| 輸入 | 說明 | 預設 |
|---|---|---|
| repo 名稱 | 通常等於網域 | 必問 |
| GitHub org 或帳號 | 必問 | |
| 網址模式 | A=自訂網域(需 CNAME 加使用者設 DNS)/B=https://<org>.github.io/<repo>/(含 base 子路徑) |
問 |
| 品牌主色 | 可後補,沒給就用模板佔位色並在回報裡註明待定 | 佔位色 |
Phase 1. 專案骨架
不用 create astro 互動精靈,直接手刻,確定性高。
<repo>/
├── package.json
├── astro.config.mjs
├── .gitignore # node_modules/ dist/ .astro/
├── scripts/
│ ├── check-design.mjs # 複製 templates/,原樣
│ ├── check-content.mjs # 複製 templates/,原樣
│ ├── indexnow-submit.mjs # 複製 templates/,原樣
│ └── gsc-submit-sitemap.mjs # Phase 4 手動跑一次
├── public/
│ ├── CNAME # 只有模式 A,內容=網域
│ ├── robots.txt # 複製 templates/,{{SITE_URL}} 換正式網址
│ ├── <indexnow-key>.txt # 檔名=金鑰、內容=同一把金鑰
│ └── favicon.svg
└── src/
├── styles/variables.css # 複製 templates/,改品牌色(oklch 與 hex 兩處都改)
├── styles/global.css # 複製 templates/
├── layouts/BaseLayout.astro # 複製 templates/
└── pages/index.astro # 極簡佔位頁
IndexNow 金鑰,每站獨立產一把:
KEY=$(openssl rand -hex 16)
printf '%s' "$KEY" > public/$KEY.txt # IndexNow 的驗證機制:檔名與內容都是金鑰
echo "INDEXNOW_KEY = $KEY" # 填進 deploy.yml 的 {{INDEXNOW_KEY}}
這把金鑰不是機密(本來就公開託管在 /<key>.txt),直接寫進 workflow env,不必進 secrets。
package.json 要點(版本以 pnpm add 實際解析為準,不要寫死過時版號):
{
"type": "module",
"engines": { "node": ">=22.12.0" },
"scripts": {
"dev": "astro dev",
"build": "node scripts/check-design.mjs && node scripts/check-content.mjs && astro build",
"check:design": "node scripts/check-design.mjs",
"check:content": "node scripts/check-content.mjs",
"check:content:all": "node scripts/check-content.mjs --all"
},
"dependencies": { "astro": "^6", "@astrojs/sitemap": "^3" }
}
build 必須串兩支守門腳本。 這是整套的施力點,拆掉就沒有強制力了。
astro.config.mjs:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://<正式網址>', // 模式 B 填 'https://<org>.github.io'
// base: '/<repo>', // 只有模式 B 需要
output: 'static',
build: { format: 'directory' },
// 模式 B 必加 filter:sitemap 會多產一筆「site 加 base、結尾無斜線」的網址,
// GitHub Pages 對它回 301,部署後驗 200 的 job 會誤判失敗。
integrations: [sitemap({ filter: (page) => page.endsWith('/') })],
});
模式 B 的三個坑
實作時踩過,寫下來省下重踩的時間:
- 有
base時站內連結一律經import.meta.env.BASE_URL組,別硬編/xxx。這是 project pages 最常見的斷鏈原因。BaseLayout.astro裡 sitemap 的<link>也要跟著改。 SITE_URL會帶子路徑,IndexNow 腳本要補尾斜線並改用相對的keyLocation。- GSC 不能用
sc-domain:。github.io的 DNS 不在你手上,只能建「網址前置字元」資源https://<org>.github.io/<repo>/,用 HTML 檔或 GA4 代碼驗證。
Phase 2. 本地驗證
pnpm install && pnpm build
build 要先印出設計守門通過的訊息才算生效。
如果為了截圖或量測起了 pnpm preview 或 dev server,收尾一定要 kill。Astro 遇到 port 被佔會自動換 port,所以洩漏的行程不會報錯,只會默默累積。
Phase 3. 建 repo、開 Pages、推上線
git init -b main && git add -A && git commit -m "chore: scaffold astro site with design gate"
gh repo create <org>/<repo> --public --source . --remote origin
gh api -X POST repos/<org>/<repo>/pages -f build_type=workflow # 409 代表已開啟,可忽略
# 模式 A 必做:artifact 裡的 public/CNAME 不會自動設定 custom domain,
# 一定要明確 PUT 一次,否則站只在 <org>.github.io 生效。
gh api -X PUT repos/<org>/<repo>/pages -f cname=<網域>
mkdir -p .github/workflows # 複製 templates/deploy.yml,換掉 {{SITE_URL}} 與 {{INDEXNOW_KEY}}
git add -A && git commit -m "ci: github pages deploy workflow" && git push -u origin main
順序刻意是「先開 Pages 再 push」,免得第一次 workflow 因為 Pages 還沒啟用而 fail。真的 fail 了就開啟後 gh run rerun <id>。
deploy.yml 的 job 鏈:build(含守門)→ deploy → verify(逐網址驗 200)→ indexnow(把 sitemap 網址送 Bing/Yandex/Seznam/Naver)→ notify-failure。
Google 不吃 IndexNow。 Google 靠 robots.txt 裡的 Sitemap: 行自動發現,另外在 Phase 4 對 GSC 提交一次。
驗證上線,沒打到 200 不准回報完成:
gh run watch --repo <org>/<repo> --exit-status
curl -s -o /dev/null -w '%{http_code}' <正式網址>/
模式 A 的 DNS 在使用者手上。先驗 workflow 全綠,然後回報要設的記錄(CNAME → <org>.github.io,apex 用 A 記錄指 185.199.108.153–185.199.111.153),並明講「自訂網域等 DNS 生效後再驗」。不可宣稱網域已經通。
上線後就去要網站地圖與發展方向。
Phase 4. 搜尋引擎收錄
向 GSC 提交 sitemap 一次。robots.txt 的 Sitemap: 行會被動發現,主動提交比較快。
pnpm add -D google-auth-library
GSC_SA_KEY=<服務帳號金鑰路徑> GSC_SITE='sc-domain:example.com' \
SITE_URL='https://<正式網址>' node scripts/gsc-submit-sitemap.mjs
不想裝套件就在 GSC 網頁介面的 Sitemaps 手動提交 sitemap-index.xml 一次,等價。
GA4 與 GSC 的後台操作都在使用者端,只引導、不代辦、不臆測已完成。
收尾
- repo 建一份精簡
CLAUDE.md:技術棧、設計規範六條、常用指令、CI 各 job 說明。 - 回報:repo 網址、上線網址(附實測 HTTP 狀態碼)、品牌色是否還是佔位、搜尋引擎提交狀態。
這個 skill 不做的事
- 不裝多餘的 integration(svelte、mdx、pagefind 等到有需求再加)。
- 不代辦 Google 或第三方後台操作。
- 不碰非 GitHub Pages 的部署方式。