惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
月光博客
月光博客
博客园 - 司徒正美
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
量子位
Recent Announcements
Recent Announcements
V
V2EX
P
Proofpoint News Feed
小众软件
小众软件
云风的 BLOG
云风的 BLOG
腾讯CDC
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
B
Blog
博客园_首页
GbyAI
GbyAI
博客园 - Franky

ALBERTAZ

How I Built a Scannable 3D QR Code Generator | Every QR Code OpenStreetMap Data Pipeline for Canvas Maps | EveryCityMap 每日关灯:今天这题理论最少 8 步 OpenStreetMap + Canvas 城市地图实践:EveryCityMap 的五个工程取舍 OpenStreetMap + Canvas 城市地图实践:EveryCityMap 的五个工程取舍 博客 CDN 迁移到 Cloudflare R2 Migrate my website to SvelteKit2 Migrate my website to SvelteKit2 漫谈编辑器 漫谈编辑器 Pure ESM package Pure ESM package 又折腾博客了 又折腾博客了 Rewrite my website with Svelte Rewrite my website with Svelte 字体漫谈-网站字体最佳实践 字体漫谈-网站字体最佳实践 你不知道的 Web Components - 现状 你不知道的 Web Components - 现状 你不知道的 Web Components - 过去和未来 你不知道的 Web Components - 过去和未来 Node 应用本地 HTTPS Node 应用本地 HTTPS docker 部署 Gatsby 应用 docker 部署 Gatsby 应用 再提示『您的磁盘几乎已满』算我输 再提示『您的磁盘几乎已满』算我输 G6-Mobile 的前世今生 G6-Mobile 的前世今生
博客 CDN 迁移到 Cloudflare R2
https://www.albertaz.com/about · 2026-07-05 · via ALBERTAZ

又折腾博客了。

这次不是重写框架,也不是换主题, 而是把博客的静态资源从又拍云迁移到了 Cloudflare R2

之前我在《又折腾博客了》 里写过,从七牛云换到又拍云, 是因为又拍云联盟给的对象存储和 CDN 额度都挺香, 还顺手解决了 WebP 自适应的问题。

但几年过去,事情又变了。

一方面,域名 DNS 已经迁到了 Cloudflare。 另一方面,国内备案相关的东西我越来越懒得维护。 又拍云这套 CDN/云存储方案对备案域名还是有依赖, 所以继续放在那里,总感觉后面还要再折腾一次。

那不如现在折腾。

R2 最吸引我的地方其实不是“对象存储”,而是它跟 Cloudflare 的整合。

我本来就已经把 albertaz.com 托管到了 Cloudflare,静态资源继续走 cdn.albertaz.com,对外 URL 不变,历史文章里的图片也不用批量改。

迁移后大概就是:

Plain text
cdn.albertaz.com
  -> Cloudflare R2 custom domain
  -> albertaz-cdn bucket
  -> draw/
  -> img/

这次也只迁移真正用到的 draw/img/。 又拍云里的日志目录之类的东西,就没有必要搬家了。

R2 还有个好处是没有出口流量费。 虽然请求次数和存储仍然是账单项, 但对我这种自娱自乐的小站来说, 只要不要被人恶意刷,基本是一个挺省心的方案。

当然,“基本省心”和“完全不管”不是一回事, 所以后面还是做了几层防护。

怎么确认已经切到 Cloudflare

CDN 迁移最怕一种情况:你以为切完了,实际上请求还在旧服务上。

我主要看响应头:

Bash
curl -I https://cdn.albertaz.com/draw/avatar.jpg

如果能看到:

Plain text
server: cloudflare
cf-ray: ...
cf-cache-status: ...

基本就可以确认请求已经经过 Cloudflare。

再看 WebP:

Bash
curl -I https://cdn.albertaz.com/draw/avatar.webp

返回里如果有:

Plain text
content-type: image/webp
server: cloudflare

说明预生成的 WebP 也已经能从新的 CDN 域名访问。

最后我还会测一下 WAF:

Bash
curl -I 'https://cdn.albertaz.com/draw/avatar.jpg?x=1'

这个请求应该返回 403。因为带 query string 的图片请求很容易绕过缓存, 对图床来说没有什么价值,反而增加被刷的风险。

图片处理:不再依赖运行时参数

以前又拍云最方便的一点,就是图片处理可以写在 URL 里:

Plain text
!/format/webp
!small

比如 Markdown 里还是原图,但构建时自动追加 !/format/webp。 画图页面的封面图也会挂一个 !small 后缀,让又拍云在运行时处理缩略图。

好处是省事。

坏处也是省事:它把图片处理逻辑藏在了 CDN 服务里。

迁到 R2 后,我没有继续找一个新的运行时图片处理服务来替代它。 Cloudflare 有 Polish,也有 Images, 但对这个博客来说,我最后还是觉得本地预处理更合适。

现在图片源文件放在:

Plain text
.cdn-source/draw/
.cdn-source/img/

日常只需要:

Bash
pnpm cdn:dry-run
pnpm cdn:publish

cdn:dry-run 是预演,会告诉我哪些图片会生成 WebP,哪些文件会上传到 R2, 但不会真的写文件,也不会真的上传。

确认没问题后,再跑 cdn:publish。它会用 sharp 生成 WebP,再用 rclone copy 上传原图和 WebP。

这里故意用的是 copy,不是 sync。 我不希望一个手滑就把 R2 上的文件删掉。

如果在集中整理图片,也可以开:

Bash
pnpm cdn:watch

把图片丢进 .cdn-source/,它会自动发布。很朴素,但够用。

代码怎么兼容 WebP

文章里还是正常写原图:

Markdown
![example](https://cdn.albertaz.com/img/example.png)

构建时会把 CDN 上的 JPG/PNG 自动包成 <picture>

HTML, XML
<picture>
  <source srcset="https://cdn.albertaz.com/img/example.webp" type="image/webp">
  <img src="https://cdn.albertaz.com/img/example.png" alt="example">
</picture>

这样写文章的时候不用想 WebP,浏览器支持就加载 WebP,不支持就回退原图。

这也是我比较喜欢的状态:内容还是内容,优化交给构建流程。

防刷和预算控制

R2 没有出口流量费,但不代表可以完全裸奔。

最后我给 cdn.albertaz.com 做了几层限制:

第一层是 Cloudflare Hotlink Protection,用来挡常见外站盗链。

第二层是 WAF 自定义规则:

  • 只允许 GETHEAD
  • 只允许 /draw//img/
  • 拒绝 query string。
  • 只允许图片扩展名:.jpg.jpeg.png.gif.svg.webp

第三层是 Rate Limiting:

Plain text
100 requests / 10 seconds

范围只匹配 cdn.albertaz.com 下的 /draw//img/, 并排除 Cloudflare verified bots。 这个阈值没有设得特别激进, 主要是挡明显不正常的请求,不想误伤正常浏览。

第四层是缓存头:

Plain text
Cache-Control: public, max-age=31536000, immutable

我的图片文件名基本不会复用,所以长期缓存是适合的。 边缘缓存命中越多, R2 的读取次数也就越少。

为什么不用 GitHub Actions

中间我还想过做 GitHub Actions 自动化, 类似一些 S3 sync 方案:代码一推,CI 跑起来, 同步到对象存储。

但!

这套博客图片流程里,.cdn-source/ 是本地目录,不会提交到 Git 仓库。

如果 GitHub Actions 要生成 WebP, 就只能先从 R2 把源图拉下来,再处理,再传回 R2。 这听起来自动化了,但实际是在白白增加 R2 的列目录和读取操作。

所以最后还是撤掉了 GitHub Actions,改成本地发布:

Plain text
本地源图 -> 本地生成 WebP -> 上传 R2

老实说,这一点也不 fancy,但很符合我的使用方式。

PicGo 可以用吗

PicGo 这种图床工具当然很好用, 拖拽上传、自动复制链接, 体验很顺。

但它更适合临时分享图片,不太适合当这个博客的主流程。

因为如果图片直接从 PicGo 上传到 R2,本地 .cdn-source/ 就没有这张源图。 后面的 WebP、未来可能加的 AVIF、缩略图、manifest,也就都断开了。

所以我现在的取舍是:

  • 写博客用的图片:先放 .cdn-source/,再 pnpm cdn:publish
  • 临时分享用的图片:可以用 PicGo。
  • 如果 PicGo 上传的图片以后要写进博客,最好再补回 .cdn-source/

总结

这次迁移之后,博客的静态资源变成了:

Plain text
Cloudflare R2 负责存储
Cloudflare CDN/WAF 负责分发和防护
本地脚本负责图片处理和发布

从体验上看,其实没有“又拍云一行 URL 参数”那么省事。

但换来的好处是图片处理逻辑回到了项目里,迁移成本更低, 未来要加 AVIF、缩略图或者别的处理, 也不会被某个云服务的 URL 参数绑住。

以上,希望这版图床能活得更久一点。