写博客最容易走的弯路,是从”我要做一个博客”开始。
于是你开始:初始化项目、写 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。
git clone https://github.com/saicaca/fuwari.git my-blogcd my-blogrm -rf .git && git initWARNING这就是”学会助力”的意思。学生思维是”这个东西我不会,我要去学”。工程师思维是”这个东西有人做好了,我要去用”。
我学到的东西不会消失,借来的能力也不会。区别只在于我把时间花在了写文章上,而不是写
<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 Checkon: 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 qualityjobs: 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 |
| ❌ 不参与(走 Search Console sitemap 提交) | |
| 归属验证 | 根目录放一个 <key>.txt,内容等于文件名 |
归属验证那一招很妙 —— 不需要 DNS TXT、不需要账号,只要你的站点上有一个文件。换域名也不用重新验证,把文件一起部署就行。
name: IndexNowon: 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: 2updates: - 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 忽略 —— 大版本变更必须人工判断,不能自动合。
三、整条链路跑起来的样子
四、我在这个过程中做的减法
一开始我的 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 build | Pages 每次 push 都构建,重复且更慢 |
pnpm test | 没有测试文件,红的 |
NOTECI 的成本不是分钟数,是维护成本。 一个总是红的 CI 会被忽略,一个没人看的 CI 等于没有。每加一条流水线,就多一份将来会过期、会误报、会让你养成 rerun 习惯的负担。
删掉冗余检查之后,CI 变成了它该有的样子:一个纯粹的、信号明确的红绿灯。 红了就是真出问题了。
小int小结
选型 → 官方推荐的框架(Cloudflare 文档里有 Astro 集成)起步 → fork 一个完成度高的 MIT 主题,别从 <head> 开始CI 1 → astro check:类型检查(Pages 不管这个)CI 2 → biome ci:代码质量,reporter 直出 PRCI 3 → IndexNow:免费、无需账号、主动推送搜索引擎不做 → 不跑 build(Pages 已经跑了)、不跑矩阵(就一个版本)、不跑没用的 test记得 → action 钉 SHA、--frozen-lockfile、推送失败 continue-on-error、 push 之后先等部署上线再推索引用别人的框架 + 让 CI 守住底线,我在这个博客上的”工程投入”大概是一个周末。剩下的全是写文章。
这才是正确的比例。