之前我博客里的图片基本都丢在 Jerry-FaGe/jerry-fage-static 这个 GitHub 仓库里,然后通过 jsDelivr 直接引用。刚开始图片少,目录随便放放也没啥问题,反正能显示就行。

直到后来我看了一眼仓库结构:img/posts 下面一堆 pasted-1.png生成API Key.png迫害1.png,好家伙,这谁顶得住

所以这次干脆趁文章和图片还没多到不可收拾,把图床仓库、博客引用、PicGo 上传流程一起捋了一遍。本文就当是这次折腾的复盘,防止以后我又忘了自己为啥这么设计。

本文不是“从零教你搭图床”的教程,而是一次真实的博客资源治理现场。关键词:GitHub 静态资源仓库、jsDelivr、Hexo、Butterfly、PicGo、以及一些该死的缓存。

一开始的问题

原来的仓库结构大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jerry-fage-static/
├── README.md
├── img/
│ ├── blog/
│ │ ├── about/
│ │ └── skills/
│ ├── cover/
│ │ ├── default_cover_1.webp
│ │ ├── default_cover_2.webp
│ │ └── ...
│ └── posts/
│ ├── pasted-1.png
│ ├── pasted-2.png
│ ├── 生成API Key.png
│ ├── 复制API Key.png
│ └── 监控BUG.jpg
└── js/
└── about.js

不能说完全没设计,只能说充满了随缘人生

主要问题有几个:

文章图片全堆在 img/posts,时间一长根本不知道哪张图属于哪篇文章

文件名中英文混着来,有些 URL 被编码后看着像上古咒语

默认封面、关于页资源、文章资源虽然有分类,但层级不够清晰

这个仓库以后可能不只存博客资源,根目录直接放 imgjs 不太优雅

尤其是 pasted-1.png 这种文件名,过几个月再看,谁知道它是啥?除非点开图片看一眼。问题是图一多,点开也烦。

先定新结构

我一开始的想法是直接把旧结构删了,全部迁移到新目录。但很快意识到这事不能这么莽。

旧链接还在博客里被引用,如果资源仓库先删旧路径,博客页面就会先炸。正确顺序应该是:先新增新结构并保留旧结构,替换博客引用并验证无误后,再考虑删旧路径。

最后定下来的新结构是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
jerry-fage-static/
├── blog/
│ ├── README.md
│ ├── img/
│ │ ├── covers/
│ │ │ └── default/
│ │ │ ├── default-cover-001.webp
│ │ │ ├── default-cover-002.webp
│ │ │ └── ...
│ │ ├── pages/
│ │ │ └── about/
│ │ │ ├── me.png
│ │ │ ├── education.png
│ │ │ ├── skills/
│ │ │ └── bangumi/
│ │ ├── posts/
│ │ │ ├── chatgpt-api-first-look/
│ │ │ ├── sign-in-with-apple-python-code-verify/
│ │ │ └── temporary-backup/
│ │ └── screenshots/
│ └── js/
│ └── pages/
├── shared/
│ └── README.md
├── img/ # 旧路径,暂时保留
└── js/ # 旧路径,暂时保留

几个设计点:

博客专用资源统一放在 blog/ 下面。这样这个仓库以后如果还想放别的项目资源,不会跟博客图片混在一起。

1
2
3
blog/img/posts/
blog/img/pages/
blog/img/covers/

shared/ 用来放多个项目都可能复用的通用资源,比如头像、通用图标、占位图之类的。

目前暂时只有 README,先把坑位占上。

旧的 img/js/ 不删,先保留。等博客引用全部替换完、构建验证没问题、线上也稳定一段时间后,再考虑清理。

这叫稳,不叫怂。

历史图片怎么归档

旧文章图片不多,所以直接按文章拆了目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
blog/img/posts/chatgpt-api-first-look/
├── 01-create-api-key.png
└── 02-copy-api-key.png

blog/img/posts/sign-in-with-apple-python-code-verify/
├── 01-find-identifier.png
├── 02-create-key.png
└── 03-download-private-key.png

blog/img/posts/temporary-backup/
├── 01-sentry-monitoring.jpg
├── 02-qq-query-1.png
├── 03-qq-query-2.png
├── 04-register-custom-event.jpg
└── 05-hook-sent-message.jpg

默认封面也顺手从:

1
img/cover/default_cover_1.webp

改成:

1
blog/img/covers/default/default-cover-001.webp

这样排序和肉眼识别都舒服不少。

关于页的技能图标也收进了 about 下面,因为它本来就是关于页的一部分:

1
2
3
blog/img/pages/about/skills/linux.png
blog/img/pages/about/skills/nginx.png
blog/img/pages/about/skills/vscode.png

替换博客里的引用

资源仓库整理完之后,下一步就是改博客源码里的 URL。

比如旧引用:

1
https://cdn.jsdelivr.net/gh/Jerry-FaGe/jerry-fage-static@master/img/posts/pasted-1.png

替换成:

1
https://cdn.jsdelivr.net/gh/Jerry-FaGe/jerry-fage-static@master/blog/img/posts/sign-in-with-apple-python-code-verify/01-find-identifier.png

涉及到的文件主要是这些:

1
2
3
4
5
6
7
_config.butterfly.yml
source/_data/about.yml
source/_data/creativity.yml
source/_posts/ChatGPT API 简单体验.md
source/_posts/Hexo Frontmatter 速查手册.md
source/_posts/Sign in with Apple(苹果授权登陆)python 服务端 code 验证.md
source/_posts/暂时做一些东西的备份.md
替换规则简单记一下
1
2
3
4
5
6
7
8
9
10
11
img/cover/default_cover_N.webp
=> blog/img/covers/default/default-cover-NNN.webp

img/blog/about/*
=> blog/img/pages/about/*

img/blog/skills/*
=> blog/img/pages/about/skills/*

img/posts/*
=> blog/img/posts/<post-slug>/*

替完之后跑了一遍:

1
2
hexo clean
hexo server

构建没炸,抽样访问新图片 URL 也都是 200。这一步很重要,不然你以为自己整理完了,结果一上线全是裂图,那就成赛博装修事故了。

要不要顺手换 CDN?

本来还想趁这次改路径,顺便看看有没有更适合国内访问的 CDN。测了一圈之后结论是:继续用 jsDelivr 主域名

测过这些:

1
2
3
4
5
6
cdn.jsdelivr.net
fastly.jsdelivr.net
gcore.jsdelivr.net
raw.githubusercontent.com
cdn.statically.io
raw.gitmirror.com

本机测试里 raw.githubusercontent.com 甚至还挺快,但这玩意儿在国内网络环境下懂的都懂,不能拿本机结果当生产结论。

后面用中国探针测了一下,cdn.jsdelivr.net 整体可用性还是最稳的。fastly.jsdelivr.netgcore.jsdelivr.net 不是不能用,但没明显优势;raw.githubusercontent.com 和一些镜像域名更不适合当长期正式链接。

所以最后决定不换域名,只换路径:

1
https://cdn.jsdelivr.net/gh/Jerry-FaGe/jerry-fage-static@master/blog/img/...

如果以后真想再提升国内访问速度,那就不是换个 GitHub 镜像域名能解决的了,应该考虑:

腾讯云 COS + CDN

阿里云 OSS + CDN

七牛云

又拍云

这就是另一套成本了,先不折腾,毕竟博客不是视频网站。

PicGo 也顺手研究了一下

图床仓库整理完,下一个问题就来了:以后图片怎么上传才不把新结构再次搞成垃圾堆?

PicGo 的 GitHub 图床配置里有个 path,比如我现在可以固定写成:

1
blog/img/posts/

然后上传图片。问题在于,PicGo 默认不知道“我现在正在写哪篇文章”。也就是说,它不会自动帮我把图片塞进:

1
blog/img/posts/<post-slug>/

除非我借助插件,或者每篇文章建一份配置。每篇文章建配置就算了,太蠢了,写几篇文章之后配置列表能比文章还长。

于是我去翻了一圈 PicGo 插件。

picgo-plugin-folder-name 看起来很符合需求,本地文件夹叫啥,它就给你加什么目录前缀。

但是它有个比较坑的问题:单张上传可以,多张上传会失效。它的 issue 里就有人提过“单次上传多个文件时,插件不起作用”。

我还顺手看了下源码,里面有个循环条件大概长这样:

1
2
3
for (let i = ctx.output.length - 1; i === 0; i--) {
// ...
}

这个条件一看就不太对。多张图的时候 i 一开始就不是 0,循环直接不执行。好家伙,怪不得。

picgo-plugin-rename-file 更靠谱一点,它支持用变量重命名,比如:

1
2
3
4
{localFolder:1}/{origin}
{y}/{m}/{d}/{origin}
{hash}
{rand:6}

如果我本地目录严格按文章 slug 来放,它确实能自动生成类似 post-slug/01-demo.png 的路径。

但这又引出另一个问题:我并不想为了上传图片,长期在博客源码里维护一套完整图片目录。

PicGo-Server 能提供 HTTP 上传接口,但它本质上还是走 PicGo 当前图床配置,不能很自然地在每次上传时动态覆盖 GitHub 的 path

当然可以继续写脚本绕,但那就从“整理图床”变成“开发一个图床系统”了,没必要。

最后我还是选择一个更朴素但稳定的流程:上传前手动重命名完整相对路径

最终决定的上传流程

PicGo 的 GitHub 图床固定配置:

1
2
3
4
仓库名:Jerry-FaGe/jerry-fage-static
分支名:master
存储路径:blog/img/posts/
自定义域名:https://cdn.jsdelivr.net/gh/Jerry-FaGe/jerry-fage-static@master/blog/img/posts

写文章时,如果需要本地预览,就先放到:

1
source/post-assets/<post-slug>/

比如:

1
2
3
source/post-assets/blog-static-assets-picgo/cover.webp
source/post-assets/blog-static-assets-picgo/01-old-structure.png
source/post-assets/blog-static-assets-picgo/02-new-structure.png

文章里临时这么写:

1
![旧目录结构](/post-assets/blog-static-assets-picgo/01-old-structure.png)

等文章写完,图片也确定不改了,再用 PicGo 上传。上传前在 PicGo 里把文件名改成:

1
2
3
blog-static-assets-picgo/cover.webp
blog-static-assets-picgo/01-old-structure.png
blog-static-assets-picgo/02-new-structure.png

因为 PicGo 的基础路径已经是 blog/img/posts/,所以最终远端路径就会变成:

1
2
3
blog/img/posts/blog-static-assets-picgo/cover.webp
blog/img/posts/blog-static-assets-picgo/01-old-structure.png
blog/img/posts/blog-static-assets-picgo/02-new-structure.png

文章里的图片再替换成:

1
![旧目录结构](https://cdn.jsdelivr.net/gh/Jerry-FaGe/jerry-fage-static@master/blog/img/posts/blog-static-assets-picgo/01-old-structure.png)

这个流程不花哨,但优点是可控。目录是谁、文件叫什么、最终 URL 是什么,上传前就能看明白,不依赖插件猜我的意图。

为什么不用 post_asset_folder

Hexo 其实已经开了 post_asset_folder: true。如果文章叫:

1
我的文章.md

那对应资源文件夹可以是:

1
我的文章/

然后 cover: cover.webp 这种写法,Butterfly 也能处理成文章路径下的图片。这个能力本身没问题,但对我现在的需求不太合适。

原因很简单:我不想让文章图片长期留在博客源码仓库里

本地图片只是写文章时临时预览用,最终还是应该上传到静态资源仓库,然后文章里保留 CDN URL。这样博客源码仓库更干净,资源仓库也能统一管理图片。

所以 source/post-assets/ 对我来说只是一个临时工作区,后续可以直接加到 .gitignore,不用跟着文章一起提交。

还有一个缓存坑

GitHub + jsDelivr 这个组合用起来很爽,但缓存要记住一点:发布后的同名图片不要覆盖

比如已经上线了:

1
blog/img/posts/blog-static-assets-picgo/cover.webp

后面发现图做错了,最好不要直接覆盖这个文件,而是上传一个新文件:

1
blog/img/posts/blog-static-assets-picgo/cover-v2.webp

然后改文章引用。

@master 分支路径虽然方便,但 CDN 缓存不是你想刷新就马上刷新。对已经发布的图片,换文件名比强行等缓存刷新靠谱得多。

我现在的命名习惯大概是这样:

1
2
3
4
5
cover.webp
cover-v2.webp
01-old-structure.png
01-old-structure-v2.png
02-new-structure.png

不搞随机字符串,主要是随机名看着难受。文件名只要能表达图片内容,再加上必要的 v2v3,基本就够用了。

这次实际做了什么

本次折腾记录

整理静态资源仓库

新增 blog/shared/ 命名空间,把博客图片、页面资源、默认封面重新归档。旧 img/js/ 暂时保留,避免线上旧链接直接失效。

写中文 README

把资源仓库 README 改成中文,并且给 blog/shared/ 各补了说明。以后再打开这个仓库,不至于靠记忆猜目录用途。

替换博客引用

把 Butterfly 配置、关于页数据、创意页数据、历史文章里的旧图床 URL 替换到新路径。

构建验证

hexo cleanhexo server,同时抽样访问新 jsDelivr URL,确认新路径能正常返回 200

研究 PicGo 流程

看了一圈 PicGo 插件,最后放弃“自动猜目录”的幻想,选择上传前手动重命名完整相对路径。

最后总结

这次折腾完之后,我现在的原则就很简单:

博客资源统一放 blog/,不要污染仓库根目录

单篇文章图片放 blog/img/posts/<post-slug>/

默认封面放 blog/img/covers/default/

页面专用资源放 blog/img/pages/<page-name>/

旧路径先保留,确认稳定后再清理

CDN 继续用 cdn.jsdelivr.net,暂时不换

PicGo 上传前手动重命名,不依赖有坑的自动目录插件

发布后的图片不要覆盖同名文件,改图就上 -v2

总之,图床这玩意儿刚开始随便放确实省事,但越往后越像家里那个“先随手放一下”的抽屉。现在整理一下不一定能一劳永逸,但至少下次我再上传图片的时候,不用面对一堆 pasted-1.png 开始怀疑人生。