正在准备人机验证...
请稍后,正在请求blog-turnstile接口
Featured image of post 图床折腾记:封面搬出仓库,结果栽在 jsDelivr 缓存上

图床折腾记:封面搬出仓库,结果栽在 jsDelivr 缓存上

把封面从博客仓库搬到独立图床仓库,走 jsDelivr 加速,顺手记下缓存不刷新和节点不一致两个坑。

起因:几张大图把仓库拖进了泥潭

一开始封面图是老老实实放站内的,路径在 static/img/covers/ 下面,构建的时候原样复制到 public/,用起来最省心——不用管外链,不用管 CDN,image: 写个相对路径就完事。

问题出在体积上。一张 1200×675 的 JPG,压到差不多 130KB 已经算克制了,但文章一篇篇往上加,图片就跟着一篇篇往下堆。等到某次 上传.bat 跑完,我发现这个仓库越来越像半个相册:源码、主题、构建产物、图片全挤在一个 git 历史里,删都删不干净——git 不记得你删了什么,它只记得你曾经提交过什么。

更麻烦的是 public 那个产物仓库。它是每次构建整份推上去的,图片在里面各存一份,源码仓库再存一份,等于同一张图占了两份配额。GitHub Pages 对仓库大小虽然不是硬卡,但推得越久越难受。

于是有了这次搬迁:图归图,文章归文章。

选型:为什么最后还是选了 GitHub 仓库 + jsDelivr

图床这事我盘了一圈,路子大概三类。

一类是纯第三方图床,注册就送空间。好处是省事,坏处是稳不稳定全看别人心情,哪天跑路或者开始限流,文章里的图就集体变白框——而且这种白框是历史文章里的,你很难一篇篇回头去换。

一类是对象存储,Cloudflare R2、又拍云、七牛这些。功能都对,但对一个纯静态博客来说有点重:要开账号、配密钥、设 CORS、算流量,我这种连域名都还没舍得买的人,为几张贴图去折腾一整套云服务,性价比不高。

最后一类就是拿一个公开的 GitHub 仓库当存储,再套一层 jsDelivr 做加速。选它的理由很朴素:仓库本来就有,jsDelivr 免费、有全球节点、支持 @版本号 和分支引用,而且因为它是"从 git 里读文件",只要仓库是公开的,链接就永久有效,不会像某些图床那样过两年开始收费清图。

缺点也明摆着——jsDelivr 对 GitHub 源有缓存,而且是那种"缓存得挺认真"的类型。这一点后面坑了我一次。

目录按年月切,别堆一层

新建仓库 hugo-pic-bed,公开,什么都不用配,README 随便写两句,然后开始定目录。

平铺一层是最省事的,但一年下来几百个文件名挤在一起,找起来眼睛疼。所以我按上传的年月分层:

1
2
3
4
5
6
2026/
  09/
    2026-09-25-cover.jpg
    2026-09-26-cover.jpg
  10/
    2026-10-29-cover.jpg

文件名统一用 日期-用途.jpg,好处是永远不会重名,排序天然按时间,而且看文件名就知道这张图是哪篇文章的。这个命名习惯后来救了我一次——见缓存那节。

上传:从拖拽到 API 直传

最开始是网页端手动传:打开仓库,点 Upload files,把图拖进去,写 commit message,提交。传一两张还行,赶上批量出封面的时候就有点烦,而且网页端对文件名里的空格和中文处理得不怎么友好。

后来改成走 GitHub 的 Contents API,PUT /repos/{owner}/{repo}/contents/{path},把图片 base64 编码塞进请求体,带 token 认证。好处是可以脚本化,传完顺手就能验证。限制也记一下:接口对单文件有大小上限,图片这种几百 KB 的量级完全够用,真要传大文件就该上 Git LFS 或者 Release 附件了。

有一点值得强调:base64 会让请求体膨胀约三分之一,所以别拿它传几十兆的东西,会超时。

第一个坑:jsDelivr 缓存不刷新

图片传上去,链接拼出来是这种形式:

1
https://gcore.jsdelivr.net/gh/YrqSpace/hugo-pic-bed@main/2026/09/2026-09-25-cover.jpg

第一次访问,正常。然后我改了同一张图(重新生成了一版封面),覆盖提交,刷新页面——还是旧的那张。

我一开始以为是浏览器缓存,Ctrl+F5、无痕窗口、换设备,全试了,还是旧的。后来才想明白问题在哪:@main 这种分支引用,jsDelivr 会缓存一段时间,你覆盖同名文件,CDN 那边并不知情。它甚至不是"没更新",而是"根本不打算马上更新"。

这里有两个解法,我现在是两个一起用:

一是给文件起唯一的名字。 因为我的命名规则是 日期-用途.jpg,每次重新生成要么换日期、要么加后缀,天然避开了"同名覆盖"这个动作。这是最省事的做法——不是去解决缓存,而是让缓存无从下手。

二是主动清缓存。 jsDelivr 有专门的 purge 接口,访问一次对应路径就会让它去源站重新拉:

1
https://purge.jsdelivr.net/gh/YrqSpace/hugo-pic-bed@main/2026/09/2026-09-25-cover.jpg

返回一段 JSON,说明清了几个节点。这个适合"文件名实在不想改"的场景。

顺带说一句,@main 换成具体的 commit hash 也能绕过缓存,因为 URL 变了——但那意味着每次提交都要改文章里的链接,比改名还麻烦,我没用。

第二个坑:两个节点给的东西不一样

排查缓存的时候,我还撞上一个更迷惑的现象:同一个文件,gcore.jsdelivr.net 和 cdn.jsdelivr.net 打开来不一样,一个已经是新的,一个是旧的。

原因是这两个域名解析到不同的节点,各自的缓存是独立的。你清了其中一个,另一个还是老样子。所以现在我的做法是固定用一个域名,全站统一 gcore.jsdelivr.net,别混着来——混用会导致"我这里看着是新的,别人那里是旧的"这种最难查的问题。

至于哪个节点快,取决于你所在地区,这个只能自己试。国内访问 jsDelivr 有时候会抽风,备选方案是把路径换成别的镜像,但那是另一个话题了。

怎么确认真的传上去了

这是整个流程里最容易被跳过、但也最该有的一步:别靠眼睛看,靠响应头。

1
curl -I https://gcore.jsdelivr.net/gh/YrqSpace/hugo-pic-bed@main/2026/09/2026-09-25-cover.jpg

看三样东西:状态码是不是 200,content-length 是不是和本地文件对得上,age 头是多少(缓存了多少秒)。内容长度对不上,八成就是读到旧缓存了。

因为封面是给文章 image: 字段用的,一旦链接挂了就是文章顶部一片空白,所以每次传完我都会跑一次这个检查,确认字节数一致才去改 front matter。这个习惯比事后发现图裂再回头找要省事得多。

现在的流程

整理一下,现在是这样跑的:

步骤 做什么 注意
1 本地生成封面,压到 130KB 左右 尺寸统一 1200×675
2 按 年/月/日期-用途.jpg 命名 名字唯一,绕开缓存
3 走 API 上传到 hugo-pic-bed base64 编码,注意体积
4 curl -I 验证状态码和字节数 对不上就 purge
5 把 CDN 链接写进文章 image: 固定用同一个节点域名

搬完之后舒服的地方很明显:博客仓库小了一圈,图片有独立的历史,删图不影响文章提交记录,CDN 那边还顺手把加载速度提了一点。坑也确实踩了——缓存和节点这两个,都属于"不知道的时候会怀疑人生,知道了就是一句话的事"。

下次再碰到图不更新,先别怀疑自己传错了,去 purge 一下、或者干脆换个文件名,八成就是它。

共 12 篇文章 · 约 2.0 万字 · 运行 40 天
Build using Hugo