博客最容易变重的地方不是文章,是图片。截图、录屏、PDF 一旦提交进 Git,仓库会以肉眼可见的速度膨胀,构建也会变慢。

所以这个站的媒体文件全部放在 Cloudflare R2,仓库里只有 Markdown。

为什么不直接把图片放仓库

  • 仓库体积:Git 会永久保存历史版本,删掉图片也不会变小。
  • 构建速度:每次构建都要读取这些静态资源。
  • 分发质量:R2 背后是 Cloudflare 的边缘网络,缓存和带宽比 Pages 的静态资源更从容。

桶里的目录约定

整个桶叫 images,但只给博客用 blog/ 前缀,其他内容互不干扰:

images/
├── blog/
│   ├── posts/
│   │   └── 2026/
│   │       └── media-workflow/     # 每篇文章一个目录
│   ├── covers/
│   │   └── 2026/                   # 文章封面,按年份归档
│   └── site/                       # 头像、默认 OG 图等站点级素材
└── testFiles/                      # 已有的临时文件,不动

按文章分目录的好处是:删除或迁移一篇文章的媒体只需要动一个前缀,不会误伤别的文章。

上传流程

媒体先放在本地一个临时目录(例如 ./media),运行上传脚本:

npm run r2 -- ./media --prefix blog/posts/2026/media-workflow

脚本会做四件事:

  1. 递归遍历目录,跳过隐藏文件和构建产物;
  2. 计算文件内容哈希,生成带指纹的文件名,例如 diagram-9f2c1a4b.png;
  3. 上传到指定前缀下,并设置一年的 immutable 缓存;
  4. 在终端打印可直接粘贴的 URL。

得到的链接像这样:

https://store.yky.de5.net/blog/posts/2026/media-workflow/diagram-9f2c1a4b.png

把它写进文章即可:

![结构图](https://store.yky.de5.net/blog/posts/2026/media-workflow/diagram-9f2c1a4b.png)

常用参数

参数作用
--dry-run只预览将要上传的文件和新链接,不真正上传
--force忽略本地清单,强制重新上传
--no-hash使用原文件名覆盖上传,适合需要固定链接的场景
--prefix指定 R2 里的目录前缀,例如 --prefix blog/posts/2026/hello-world

默认使用带哈希的文件名。内容是全新的 URL,所以可以放心设置长期强缓存,不会出现改了图但浏览器还显示旧图的问题。

跳过重复上传

脚本在本地维护一份 .r2-manifest.json,记录每个文件的路径、大小、修改时间和哈希。文件没变就直接跳过,重复执行没有副作用。

这份清单不会提交到 Git,它只服务于本机的上传过程。换一台电脑第一次运行时会重新上传一次,属于正常现象。

安全边界

上传用的 R2 令牌只授予单个桶的 Object Read & Write,不涉及账号级管理权限;密钥只存在本地 .env,不进仓库、不进构建产物。静态博客在运行时完全不接触 R2 的凭证。