起因:几张大图把仓库拖进了泥潭
一开始封面图是老老实实放站内的,路径在 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 随便写两句,然后开始定目录。
平铺一层是最省事的,但一年下来几百个文件名挤在一起,找起来眼睛疼。所以我按上传的年月分层:
|
|
文件名统一用 日期-用途.jpg,好处是永远不会重名,排序天然按时间,而且看文件名就知道这张图是哪篇文章的。这个命名习惯后来救了我一次——见缓存那节。
上传:从拖拽到 API 直传
最开始是网页端手动传:打开仓库,点 Upload files,把图拖进去,写 commit message,提交。传一两张还行,赶上批量出封面的时候就有点烦,而且网页端对文件名里的空格和中文处理得不怎么友好。
后来改成走 GitHub 的 Contents API,PUT /repos/{owner}/{repo}/contents/{path},把图片 base64 编码塞进请求体,带 token 认证。好处是可以脚本化,传完顺手就能验证。限制也记一下:接口对单文件有大小上限,图片这种几百 KB 的量级完全够用,真要传大文件就该上 Git LFS 或者 Release 附件了。
有一点值得强调:base64 会让请求体膨胀约三分之一,所以别拿它传几十兆的东西,会超时。
第一个坑:jsDelivr 缓存不刷新
图片传上去,链接拼出来是这种形式:
|
|
第一次访问,正常。然后我改了同一张图(重新生成了一版封面),覆盖提交,刷新页面——还是旧的那张。
我一开始以为是浏览器缓存,Ctrl+F5、无痕窗口、换设备,全试了,还是旧的。后来才想明白问题在哪:@main 这种分支引用,jsDelivr 会缓存一段时间,你覆盖同名文件,CDN 那边并不知情。它甚至不是"没更新",而是"根本不打算马上更新"。
这里有两个解法,我现在是两个一起用:
一是给文件起唯一的名字。 因为我的命名规则是 日期-用途.jpg,每次重新生成要么换日期、要么加后缀,天然避开了"同名覆盖"这个动作。这是最省事的做法——不是去解决缓存,而是让缓存无从下手。
二是主动清缓存。 jsDelivr 有专门的 purge 接口,访问一次对应路径就会让它去源站重新拉:
|
|
返回一段 JSON,说明清了几个节点。这个适合"文件名实在不想改"的场景。
顺带说一句,@main 换成具体的 commit hash 也能绕过缓存,因为 URL 变了——但那意味着每次提交都要改文章里的链接,比改名还麻烦,我没用。
第二个坑:两个节点给的东西不一样
排查缓存的时候,我还撞上一个更迷惑的现象:同一个文件,gcore.jsdelivr.net 和 cdn.jsdelivr.net 打开来不一样,一个已经是新的,一个是旧的。
原因是这两个域名解析到不同的节点,各自的缓存是独立的。你清了其中一个,另一个还是老样子。所以现在我的做法是固定用一个域名,全站统一 gcore.jsdelivr.net,别混着来——混用会导致"我这里看着是新的,别人那里是旧的"这种最难查的问题。
至于哪个节点快,取决于你所在地区,这个只能自己试。国内访问 jsDelivr 有时候会抽风,备选方案是把路径换成别的镜像,但那是另一个话题了。
怎么确认真的传上去了
这是整个流程里最容易被跳过、但也最该有的一步:别靠眼睛看,靠响应头。
|
|
看三样东西:状态码是不是 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 一下、或者干脆换个文件名,八成就是它。