2261 字
11 分钟
别从零写博客:fork 一个 Astro 模板,再让 CI 替你把关

写博客最容易走的弯路,是从”我要做一个博客”开始。

于是你开始:初始化项目、写 HTML 骨架、引入 CSS 框架、做亮/暗色主题切换、写文章列表分页、写目录导航、写 RSS、写搜索、写代码高亮、写 SEO meta……

三个月后你有一个博客框架,和零篇文章。

我这次换了个思路。


一、Astro 是 Cloudflare 官方推荐的框架#

先把选型的成本降到零:

  • Cloudflare 官方文档里有 Astro on Cloudflare 的专门集成指南
  • 静态输出直接推到 Pages,连 adapter 都不需要
  • 因为是官方推荐,社区里针对 Pages 的踩坑方案和 workaround 都是现成的

选型这一步没花我十分钟。

主题也是现成的:Fuwari(MIT),一个完成度相当高的博客主题,开箱有:

  • 亮色/暗色/跟随系统三态主题,带 localStorage 持久化
  • 标签页 / 分类聚合 / 归档 / 搜索
  • 文章目录(TOC)、上下篇导航、阅读时长
  • KaTeX 数学公式、代码高亮、代码折叠
  • 页面转场动画(Swup)
  • i18n(10 种语言)
  • 评论、站点地图、RSS
  • 图片懒加载 + 灯箱(PhotoSwipe)

这些随便挑一项从零写,都够我写一个周末。所以我的第一个动作是 fork,不是 new。

Terminal window
git clone https://github.com/saicaca/fuwari.git my-blog
cd my-blog
rm -rf .git && git init
WARNING

这就是”学会助力”的意思。学生思维是”这个东西我不会,我要去学”。工程师思维是”这个东西有人做好了,我要去用”。

我学到的东西不会消失,借来的能力也不会。区别只在于我把时间花在了写文章上,而不是写 <head> 里的 meta 标签上。

博客最重要的是博文。框架能省的时间,就该省下来。


二、CI 只做三件事,每件都有明确边界#

fork 完之后我加了三条 GitHub Actions 流水线。先说最重要的一个设计决策:

CI 里不跑 astro build#

Cloudflare Pages 每次 push 都会完整构建一次。CI 再跑一遍是纯浪费 —— 同样的依赖安装、同样的构建,跑到两台不同的机器上。

所以我做了分工:

Cloudflare Pages 负责:这份源码能不能构建成一个能用的站点
GitHub Actions 负责:这份源码本身写对了没有

构建成功与否,Pages 会在 commit 上写 status,我没必要重复验证。CI 只补 Pages 不管的那部分检查。

流水线 1:astro check —— 类型检查#

name: Astro Check
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # 连推多次时取消旧的,省 Actions 分钟数
permissions:
contents: read # 最小权限,默认 token 权限更大
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 22
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
run_install: false
- run: pnpm install --frozen-lockfile
- run: pnpm astro check

注意 @11bd7190... 这种写法 —— action 版本钉死在 commit SHA 上,注释里写上对应的 tag。

# ❌ 别人改了 tag 指向,你下次构建跑的就是新代码
uses: actions/checkout@v4
# ✅ 供应链攻击改不了你的构建产物
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

这不是洁癖。actions/checkout 这种被所有仓库引用的 action,一旦 tag 被劫持就是全网级的供应链事件。个人博客用 SHA 钉死,零成本。

TIP

--frozen-lockfile 是 pnpm 对应 npm ci 的严格模式:lockfile 和 package.json 不一致就直接失败。CI 里必须用它,否则本地偷偷改了依赖但没更新 lockfile,CI 会装出一套和本地不一样的依赖,bug 现场极难复现。

流水线 2:Biome 代码质量#

name: Code quality
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71...
- uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
with:
version: latest
- run: biome ci ./src --reporter=github

--reporter=github 让问题直接显示在 PR 的 Files Changed 里,标注到具体行。省掉”CI 红了 → 本地跑一遍 → 对照行号”这一套。

WARNING

这条流水线在 Windows 本地有个大坑。 我的工作区是 CRLF,仓库里存的是 LF。本地跑 biome ci ./src 会误报几十个格式错误,但 CI 是 Linux,是绿的。

也就是说:本地红了不代表代码有问题,但你不能因此就无视它。 要在本地验证,得先把文件转成 LF:

Terminal window
Get-ChildItem -Recurse -Path .\src -Include *.ts,*.astro,*.css,*.styl |
ForEach-Object {
$c = [IO.File]::ReadAllText($_.FullName)
[IO.File]::WriteAllText($_.FullName, $c.Replace("`r`n", "`n"), (New-Object Text.UTF8Encoding $false))
}

这是 Windows 开发最没劲的地方之一:你没做错,但工具链因为换行符给你制造了一个不存在的问题。

流水线 3:IndexNow —— 主动推送给搜索引擎#

这个是我折腾得最值的一条。

背景:Google 在 2023 年下线了 sitemap ping 接口。所以”发布完主动告诉搜索引擎”这件事,主流方案里只剩 IndexNow 还活着。

特性IndexNow
费用免费
需要注册账号吗不需要
参与方Bing、Yandex、Seznam、Naver
Google❌ 不参与(走 Search Console sitemap 提交)
归属验证根目录放一个 <key>.txt,内容等于文件名

归属验证那一招很妙 —— 不需要 DNS TXT、不需要账号,只要你的站点上有一个文件。换域名也不用重新验证,把文件一起部署就行。

name: IndexNow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71...
# ① 等 Pages 把新版本部署上去
- name: Wait for the new Pages deployment to go live
run: |
set -u
SITE="https://ycxdev.com"
for i in $(seq 1 30); do
if curl -fsS "$SITE/robots.txt" | grep -q "Sitemap"; then
echo "Site is serving: $SITE"
exit 0
fi
echo "waiting for deployment... ($i/30)"
sleep 10
done
echo "Site did not come up in time, continuing anyway"
# ② 推送
- name: Submit URLs to IndexNow
continue-on-error: true # 推送失败不该让 CI 变红
run: |
URLS=$(curl -fsS "$SITE/sitemap-0.xml" \
| grep -o '<loc>[^<]*</loc>' | sed -e 's|<loc>||' -e 's|</loc>||' \
| head -100 | jq -R . | jq -s .)
jq -n --arg host "ycxdev.com" \
--arg key "$KEY" \
--arg keyLocation "$SITE/$KEY.txt" \
--argjson urlList "$URLS" \
'{host:$host,key:$key,keyLocation:$keyLocation,urlList:$urlList}' > payload.json
curl -sS -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json; charset=utf-8" \
--data-binary @payload.json

这一步里有两个细节是我后来才补上的,都是踩出来的:

CAUTION

① 必须等部署生效再推送。 否则你告诉搜索引擎”这个 URL 有新内容了”,它立刻来抓,结果页面 404 —— 这是最糟糕的信号。Cloudflare Pages 构建要一两分钟,所以要轮询等。

② continue-on-error: true。 搜索引擎推送失败是网络波动类的错误,不应该让 CI 变红。变红了就会养成”CI 红了就 rerun”的习惯,真正的构建失败会被淹没。

附赠:Dependabot#

version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
groups:
patch-updates:
patterns: ["*"]
update-types: ["patch"]
minor-updates:
patterns: ["*"]
update-types: ["minor"]
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]

依赖更新全自动开 PR。patch 和 minor 分成两组(一次提一堆,不会让你收到几十个 PR),major 忽略 —— 大版本变更必须人工判断,不能自动合。


三、整条链路跑起来的样子#

sequenceDiagram autonumber participant ME as 我 participant GH as GitHub participant CI as GitHub Actions participant CF as Cloudflare Pages participant IX as IndexNow participant SE as Bing / Yandex ME->>GH: git push main par 两条流水线并发跑 GH->>CI: Astro Check + Biome CI->>CI: pnpm install → astro check → biome ci CI-->>GH: 状态写回 commit end GH->>CF: Webhook CF->>GH: 拉源码 CF->>CF: pnpm build → dist Note over CF: 构建失败则不动线上版本(原子部署) CF-->>SE: 线上已更新 GH->>CI: IndexNow 流水线启动 CI->>CF: 轮询 robots.txt,等新版本真的上线 CF-->>CI: 就绪 CI->>CF: 抓 sitemap-0.xml CI->>IX: POST {host, key, keyLocation, urlList} IX->>SE: 通知参与方 SE->>CF: 立刻重新抓取新文章 Note over CI: continue-on-error,推送失败不影响 CI 状态

四、我在这个过程中做的减法#

一开始我的 CI 是这样的:

strategy:
matrix:
node-version: [18, 20, 22]
steps:
- run: pnpm astro check
- run: pnpm astro build # ← 和 Pages 重复
- run: pnpm test # ← 项目里根本没有测试

跑了几次之后删到只剩一个 job、一个 step。原因很简单:

原来的检查为什么删
Node 18/20/22 矩阵Pages 只用一个版本,构建矩阵没有意义
astro buildPages 每次 push 都构建,重复且更慢
pnpm test没有测试文件,红的
NOTE

CI 的成本不是分钟数,是维护成本。 一个总是红的 CI 会被忽略,一个没人看的 CI 等于没有。每加一条流水线,就多一份将来会过期、会误报、会让你养成 rerun 习惯的负担。

删掉冗余检查之后,CI 变成了它该有的样子:一个纯粹的、信号明确的红绿灯。 红了就是真出问题了。


小int小结#

选型 → 官方推荐的框架(Cloudflare 文档里有 Astro 集成)
起步 → fork 一个完成度高的 MIT 主题,别从 <head> 开始
CI 1 → astro check:类型检查(Pages 不管这个)
CI 2 → biome ci:代码质量,reporter 直出 PR
CI 3 → IndexNow:免费、无需账号、主动推送搜索引擎
不做 → 不跑 build(Pages 已经跑了)、不跑矩阵(就一个版本)、不跑没用的 test
记得 → action 钉 SHA、--frozen-lockfile、推送失败 continue-on-error、
push 之后先等部署上线再推索引

用别人的框架 + 让 CI 守住底线,我在这个博客上的”工程投入”大概是一个周末。剩下的全是写文章。

这才是正确的比例。

别从零写博客:fork 一个 Astro 模板,再让 CI 替你把关
https://ycxdev.com/posts/github-actions-ci-and-fork-astro-template/
作者
YCX
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0