把博客图片搬出 git 仓库:R2 图床落地记录
这个博客跑在 Cloudflare Workers 上,文章是 Markdown,部署是 push 到 main 后 CI 自动 wrangler deploy。有一件事一直没定下来:图片放哪。
最常见的做法是把图片和文章一起提交进仓库(public/assets/<slug>/)。能用,但隐性成本不低:
- 仓库体积只增不减。 压得再好,几十篇文章积累下来也是几十上百 MB,clone 变慢,git 历史里的旧图永远删不掉。
- 图片和文字的发布节奏被绑死。 想先传图、看看效果、再改文字?必须整体 commit。
- 缓存头不归你管。 Worker 分发静态文件时,缓存策略要么全站一致,要么写代码特判。
所以这次直接把图片挪到 R2——Cloudflare 的对象存储,免费层 10 GB 存储、自定义域名分发不收流量费,对一个个人博客绰绰有余。
架构:两条互不干扰的线
关键取舍是两条线彻底解耦:
- 部署线只管代码和文字,每次 push 自动跑;
- 图片线只在写作时手动跑一次,上传后拿到 URL 贴进 Markdown,之后永远不需要再碰。
图片 URL 直接指向 R2 的公开自定义域名 img.awmthink.com,不经过 Worker。这意味着图片请求零 Worker 调用量、零 cookie,Cloudflare 边缘直接命中缓存。
链路搭建
R2 需要先在 Dashboard 开通一次(绑支付方式激活免费层),之后的 bucket 和上传都可以走 CLI:
# 建 bucket
npx wrangler r2 bucket create assets
# Dashboard 里给 bucket 绑自定义域名 img.awmthink.com,
# Cloudflare 自动建 CNAME 和证书,几分钟生效
本机先 npx wrangler login 做一次 OAuth 授权,之后上传就不需要任何长期凭据。
上传脚本
上传是高频动作,包了一个零依赖的小脚本,核心就是调 wrangler r2 object put:
const key = `${slug}/${basename(file)}`;
const result = spawnSync('npx', [
'wrangler', 'r2', 'object', 'put', `assets/${key}`,
'--file', file,
'--content-type', MIME[ext],
// 图片 URL 会永久留在 Markdown 里,按"内容不变"假设缓存一年;
// 改图必须换文件名,重传同名不会刷新边缘缓存
'--cache-control', 'public, max-age=31536000, immutable',
'--remote',
], { stdio: 'inherit' });
写完文章配图时只需要:
npm run upload ~/Desktop/cover.webp blog-images-on-r2
# ✅ https://img.awmthink.com/blog-images-on-r2/cover.webp
脚本里顺手做了两道闸:扩展名白名单,以及 2MB 大小上限——把“提交前压缩”从事后约定变成事前强制。
CI 部署不碰 R2
CI(GitHub Actions)只需要两个 secret:CLOUDFLARE_API_TOKEN 和 CLOUDFLARE_ACCOUNT_ID。deploy job 挂在 check 后面,只有 main 的 push 会触发:
deploy:
needs: check
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: '22', cache: npm }
- run: npm ci
- run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
R2 的凭据不进 CI:构建期不碰图片,部署期不需要 R2 权限。原则很简单——CI 只拿它真正用到的凭据,用不到的变量加进去纯粹是扩大泄露面。
踩坑:帐户令牌不能用用户端点验证
这次落地过程中浪费最久的一个坑。背景:Cloudflare 现在有两类 API 令牌——用户令牌(cfut_ 前缀,跟人走)和帐户令牌(cfat_ 前缀,跟账户走,官方推荐自动化场景用)。
我的 token 是 cfat_ 帐户令牌,但验证时用了用户令牌的端点:
# ✗ 用户令牌端点,对帐户令牌永远返回 Invalid API Token
curl .../user/tokens/verify -H "Authorization: Bearer cfat_xxx"
# ✓ 帐户令牌要用帐户端点
curl .../accounts/<account_id>/tokens/verify -H "Authorization: Bearer cfat_xxx"
结果就是:token 明明有效,却被我误判成无效,反复重建、回滚,来回折腾好几轮。教训是验证工具和被验证对象必须是同一套体系——错误端点返回的“无效”不是证据,只是答非所问。
小结
- 图片进 R2,仓库只留文字,clone 体积和缓存策略都干净了
- 上传一条命令(本机 OAuth,零长期凭据),分发走
img.awmthink.com(零 Worker 调用) - CI 与 R2 完全解耦,secrets 最小化
- 改图永远换文件名,旧的 immutable 缓存不用管
评论
评论公开可见,以 GitHub 身份署名。
还没有评论。