VitePress 视频与图片管理
本文是 CmdWise 网站文档系统的幕后故事,记录我们怎样把本地
assets/里的图片、视频自动上传到 R2,然后把文档里的相对路径替换为公共域名。
✨ 为什么要做?
- 仓库体积不断膨胀
VitePress 文档会插入大量截图、演示视频,长期累积后 Git 仓库和 Cloudflare Pages 部署包都变得臃肿。 - 部署有文件数量 & 体积上限
Pages 免费额度在对象数和总大小上有限制,上传超量会失败。 - 想充分利用 R2 的低成本存储 + CDN
同在 Cloudflare 生态,域名与缓存配置天然契合。
🛠️ 方案概览
核心要点:
- 原路径 & 原文件名:云端使用
docs/assets/<本地子目录>/<文件名>,不再使用哈希重命名,目录结构与仓库完全对应,利于 SEO / 排错。 - 增量上传:脚本为每个文件计算
sha1存入.asset-map.json。只有当文件内容发生变化或新增时才调用 R2 API,其余直接skip。 - 内容哈希仅作对比:虽然不改文件名,但仍用 hash 检测是否需要上传。
- 低侵入:作者照常写相对路径,构建阶段自动替换为 CDN URL。
关键脚本
路径:packages/docs/scripts/upload-to-r2.js
bash
# 查看帮助
node scripts/upload-to-r2.js --help
# 干跑,打印计划不执行
pnpm run assets:sync --dry
# 真正同步
pnpm run assets:sync脚本使用 @aws-sdk/client-s3 直连 R2,兼容 S3 协议。
🧰 环境 & 配置
- Node ≥18,包管理器 pnpm / npm。
- Cloudflare R2 bucket:
cmdwise-assets。 - 在项目
packages/docs目录准备.env:
ini
R2_ACCOUNT_ID=xxxxxxxxxxxxxxxx
R2_ACCESS_KEY_ID=xxxxxxxxxxxx
R2_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
R2_BUCKET=cmdwise-assets
R2_PUBLIC_BASE_URL=https://assets.cmdwise.app # 或先用原生 URL
.env请勿提交仓库,确保在.gitignore中排除。
🌐 CNAME 配置指南
- Cloudflare Dashboard → R2 › Custom Domains → Add custom domain
- 填入
assets.cmdwise.app,选择存储桶cmdwise-assets,确认。 - 系统会在同账号 DNS 里自动新增一条 CNAME:txt
类型: CNAME 名称: assets 内容: <accountid>.r2.cloudflarestorage.com Proxy Status: Proxied - 等待 DNS 生效(通常 < 1 分钟)。
- 在
.env/ Secrets 中把R2_PUBLIC_BASE_URL设置成https://assets.cmdwise.app。
若想切换域名,只需改
R2_PUBLIC_BASE_URL并重跑assets:sync,脚本会批量替换。
🔍 更多技术细节
| 主题 | 说明 |
|---|---|
| S3Client 配置 | forcePathStyle: true,避免虚拟主机风格子域名证书不匹配导致 TLS 握手失败 |
| 运行参数 | --dry 仅打印操作,--env .env.prod 指定额外环境文件 |
| 目录前缀 | 统一上传到 docs/assets/,后续若有 website 资源,可改为 website/assets/ 不影响现有逻辑 |
| 未来规划 | 预留图片压缩(WebP)/ 视频转码(WebM)钩子,在上传前处理 buf 即可 |
Cloudflare操作清单
- [x] 获取 R2_ACCOUNT_ID。

- [x] 创建 R2 存储桶
cmdwise-assets。 - [x] 创建 R2 存储桶
cmdwise-assets的访问密钥,获取R2_ACCESS_KEY_ID和R2_SECRET_ACCESS_KEY。
✅ 本地验证检查清单
- [x]
pnpm i成功,依赖无报错。 - [x]
pnpm run assets:sync --dry输出同步计划。 - [x]
pnpm run assets:sync后生成/更新.asset-map.json。 - [x] Markdown 中的
./assets/…链接已被替换为https://assets.cmdwise.app/…。 - [x] 运行
pnpm run dev,页面中的图片 / 视频正常加载。 - [x] R2 Dashboard 能看到新上传的对象。
🚀 CI 集成片段(GitHub Actions)
yaml
- name: Sync docs assets to R2
run: pnpm --filter @cmdwise/docs run assets:sync
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET: cmdwise-asstets
R2_PUBLIC_BASE_URL: https://assets.cmdwise.app放在 vitepress build 之前执行即可。
🩺 典型问题排查
| 现象 | 日志/报错 | 排查 & 解决 |
|---|---|---|
EPROTO / sslv3 alert handshake failure | CLI 报错 | 确认脚本已设置 forcePathStyle: true,并检查 endpoint 域名与证书是否匹配 |
Missing credentials | 运行时退出 | 检查 .env 或 Secrets 中 4 个 R2 变量是否填写正确 |
| 图片 404 | 浏览器加载失败 | 1) 检查 .asset-map.json 是否含 URL 2) 查看 R2 Dashboard 对象 key 是否正确 |
| 上传非常慢 | HeadObject/PutObject 大量调用 | 通常是增量失效,检查仓库是否清除了 .asset-map.json 或更换了路径前缀 |
| CDN 未刷新 | 资源改名后仍旧是旧图 | 因采用同名覆盖,Edge 可能缓存;可加版本号目录或使用哈希命名策略 |
⚖️ 方案对比与选择
| 维度 | 当前方案(原目录+文件名) | 哈希文件名方案 | Workers 代理方案 |
|---|---|---|---|
| SEO 友好 | ✅ URL 可读 | ❌ 不可读 | ✅ 保留原路径 |
| CDN 缓存 | 中等,文件名不变需短缓存或版本目录 | 极佳,可长期缓存 | 同当前方案 |
| 实现复杂度 | 低 | 中(需改写文件名+Map) | 高(需编写 Workers) |
| 本地作者体验 | 原路径即可 | 写相对路径但最终 URL 变化 | 原路径即可 |
| 增量上传实现 | 已支持 | 需结合 Map | 仍需 Map |
综合考虑 CmdWise 文档体量及可维护性,当前方案最平衡。如后续大规模改图或对缓存命中要求更高,可再评估哈希文件名方案。