最近想把博客里大量图片地址统一一下。

原来文章里很多图片都直接写死成了这个前缀:

1
https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master

这样有两个问题:

  1. 后面如果图床地址变化,需要全局替换,维护成本高
  2. 文章里反复出现同样的长前缀,可读性也比较差

所以我打算把它抽成一个统一变量,例如:

1
{{ IMG_BASE_URL }}

然后在配置里只维护一份真实地址。

没想到这个改动看起来简单,实际踩了两个坑。

目标

目标很明确:

  • 在文章里不再直接写完整图片基础地址
  • 改成统一变量:
1
{{ IMG_BASE_URL }}
  • 真实地址只在配置文件里保留一份,例如:
1
img_base_url: https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master

这样后续如果要换图床,只需要改一处。

第一步:批量替换源码中的图片前缀

先把 source/**/*.md 里的原始前缀替换成变量:

1
https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master

替换为:

1
{{ IMG_BASE_URL }}

同时在 _config.yml 中增加统一配置:

1
img_base_url: https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master

再新增一个 Hexo 脚本,在渲染前把占位符替换成真实地址。

最开始的思路是对的,但很快就遇到了第一个问题。

第一个坑:YAML front matter 解析报错

有些文章的封面图写在 front matter 里,例如:

1
cover: {{ IMG_BASE_URL }}/test/20220214093102.png

执行 hexo generate 之后,直接报错:

1
YAMLException: bad indentation of a mapping entry

类似这种报错:

1
cover: {{ IMG_BASE_URL }}/test/20220214093102.png

原因

因为 front matter 本质上是 YAML

而 YAML 里直接裸写:

1
{{ IMG_BASE_URL }}

会被当成非法映射结构处理,不是普通字符串。

解决方式

只要在 front matter 中,把整个值写成带引号的字符串即可:

1
cover: "{{ IMG_BASE_URL }}/test/20220214093102.png"

如果是 top_imgavatar 或其他 front matter 字段里用到这个变量,也同理。

结论

  • 正文中可以直接写 https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master
  • front matter 中必须加引号

第二个坑:页面里还是没有正确加载图片

YAML 报错修完之后,hexo generate 能跑了。

但是打开页面后发现图片还是加载不出来,HTML 里实际输出的是:

1
2
3
4
<img class="post_bg ls-is-cached lazyloaded"
data-src="{{ IMG_BASE_URL }}/test/20220214093102.png"
onerror="onerror=null;src='/img/404.jpg'"
src="/img/404.jpg">

这说明虽然页面已经生成出来了,但变量 根本没有被替换

浏览器当然不认识:

1
{{ IMG_BASE_URL }}

所以图片加载失败,最后触发 onerror,回退到了默认的 404.jpg

为什么会这样

一开始写的替换脚本,只处理了文章正文里的:

1
data.content

也就是说:

  • Markdown 正文里的图片链接可以替换
  • 但 front matter 中的 covertop_img 等字段没有被处理

而 Butterfly 主题里很多顶部背景图、封面图,正是从这些 front matter 字段渲染出来的。

所以虽然文章能生成,但最终 HTML 里依旧保留了:

1
{{ IMG_BASE_URL }}

正确做法:在渲染前递归替换文章对象中的字符串

最终的思路是:

  1. _config.yml 中维护 img_base_url
  2. scripts/img-base-url.js 中注册 Hexo 过滤器
  3. before_post_renderbefore_page_render 阶段,对文章对象中的字符串进行替换
  4. 既处理正文,也处理 front matter 中的字符串字段

但是这里还有一个细节坑:

不能粗暴地改整个对象的所有属性

如果直接递归遍历整个对象并强行回写所有字段,很容易碰到 Hexo 内部对象上的只读属性,例如:

1
TypeError: Cannot set property path of [object Object] which has only a getter

这说明某些字段只有 getter,没有 setter,不能直接重新赋值。

更稳的方式

替换逻辑需要满足两个条件:

  • 只处理字符串内容
  • 只在属性可写时才回写

这样就不会碰到只读属性报错。

最终思路总结

配置文件

_config.yml 中增加:

1
img_base_url: https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master

front matter 写法

1
cover: "{{ IMG_BASE_URL }}/test/20220214093102.png"

正文写法

1
![]({{ IMG_BASE_URL }}/test/20220214093102.png)

渲染阶段

通过 Hexo 脚本,把:

1
{{ IMG_BASE_URL }}

替换为真实地址。

最后如何验证

做完之后,可以执行:

1
2
npx hexo clean
npx hexo generate

然后再检查生成结果里是否还残留变量:

1
grep -R "{{ IMG_BASE_URL }}" public

如果没有输出,说明生成产物里已经没有残留占位符。

另外还可以直接检查页面里的图片链接,确认类似:

1
data-src="https://raw.githubusercontent.com/gmwzxiaotaiyang/HEXO_img/refs/heads/master/test/20220214093102.png"

而不是:

1
data-src="{{ IMG_BASE_URL }}/test/20220214093102.png"

这次排障得到的结论

这次看起来只是“给图片地址加个变量”,实际踩坑主要集中在两点:

1. YAML front matter 里变量必须加引号

错误写法:

1
cover: {{ IMG_BASE_URL }}/test/xxx.png

正确写法:

1
cover: "{{ IMG_BASE_URL }}/test/xxx.png"

2. 只替换正文不够,front matter 也要在渲染前替换

否则生成出来的 HTML 里仍然会保留:

1
{{ IMG_BASE_URL }}

浏览器无法识别,图片自然就加载失败。

总结

这次改造之后,好处还是很明显的:

  • 图片基础地址统一维护
  • 后面如果切换图床,只需要改一处配置
  • 文章内容更整洁,不用反复写一长串前缀

但同时也要记住:

  • front matter 是 YAML,不是普通文本
  • Hexo 主题使用的封面图、背景图,很多来自文章元数据,不只是正文
  • 变量替换逻辑必须覆盖正文和 front matter 两侧

后面如果再做类似的变量化处理,最好优先考虑:

  • 配置入口放哪
  • 渲染阶段在哪替换
  • YAML 和模板语法会不会互相冲突

这样就能少踩很多坑。