hugo-img-root插件开发
雏形
问题背景
Hugo 在构建站点后,所有资源都输出到 public/ 目录,浏览器访问时,/ 代表站点的根目录(即 public/ 文件夹)。因此,如果你在 Markdown 中引用图片时写成:
 |
实际生成的 HTML 会是 <img src="../images/photo.jpg">,这显然不是我们期望的位置。正确的方式应该是根相对路径 /images/photo.jpg。
PaperMod 主题默认的 render-image.html 已经对页面资源(Page Resources)和全局资源(assets/)进行了智能处理,但如果图片既不是页面资源也不在 assets/ 中,而只是放在 static/ 目录下(经典用法),那么生成的 src 可能是相对路径,导致 404。
因此,我们需要在主题默认逻辑的基础上,增加一个兜底逻辑:当资源找不到时,自动将路径转换为以 / 开头的根相对路径。
覆盖主题模板
Hugo 的模板渲染遵循优先级规则:站点根目录下的 layouts/ 会覆盖主题中的同名文件。所以,我们只需在 layouts/_markup/render-image.html 中放置自定义代码,即可覆盖 PaperMod 的默认实现。
具体做法是:
- 在站点根目录下创建
layouts/_markup/文件夹(如果不存在)。 - 从 PaperMod 主题目录中复制
render-image.html到该位置。 - 基于主题代码进行修改。
PaperMod主题
我用了PaperMod主题,修改后的文件,直接复制可用博客根目录\layouts\_markup\render-image.html :
1 | {{- $u := urls.Parse .Destination -}} |
Hugo 功能扩展
Hugo 本身没有传统意义上的”插件”机制(如 WordPress 的 .php 插件或 VS Code 的 Extension API)。但 Hugo 提供了多种强大的扩展方式,核心是通过 Hugo Modules(模块)、Shortcodes(短代码)、Partials(局部模板) 和 自定义函数 来实现功能扩展。
扩展方式
Hugo Modules(模块)—— 最推荐的扩展方式
Hugo Modules 是 Hugo 0.56.0+ 引入的依赖管理系统,基于 Go Modules 构建。它允许你将任何可复用的组件(主题、短代码、数据、静态资源等)打包为独立模块,供多个项目复用。
模块可以包含的内容
Hugo 的 7 种标准目录都可以作为模块的一部分:
| 目录 | 用途 |
|---|---|
layouts/ |
模板、短代码、Partials |
assets/ |
SCSS、JS、图片等需要 Hugo Pipes 处理的资源 |
static/ |
直接复制的静态文件(字体、图片等) |
data/ |
数据文件(JSON/YAML/TOML) |
content/ |
可复用的内容片段 |
i18n/ |
翻译文件 |
archetypes/ |
内容模板 |
创建模块的基本步骤
1. 初始化模块
hugo mod init github.com/<用户名>/<模块名> |
2. 配置模块挂载点(hugo.toml)
1 | [module] |
3. 在项目中引用模块
1 | [module] |
4. 常用命令
1 | hugo mod get -u ./... # 更新所有模块 |
本地开发技巧
使用 replacements 将远程模块替换为本地路径,方便实时调试:
1 | [module] |
或使用 Go Workspace 文件(*.work)管理本地开发环境。
Shortcodes(短代码)—— 内容层面的扩展
Shortcodes 让你在 Markdown 内容中插入复杂的 HTML/逻辑,而不破坏 Markdown 的纯净性。
创建自定义 Shortcode
在 layouts/shortcodes/ 下创建模板文件:
示例:GitHub 风格提示框
1 | <!-- layouts/shortcodes/gh-blockquote.html --> |
在 Markdown 中使用:
1 | {{< gh-blockquote type="warning" >}} |
Shortcode 传参方式
| 方式 | 示例 | 模板中获取 |
|---|---|---|
| 命名参数 | {{< mycode color="red" >}} |
{{ .Get "color" }} |
| 位置参数 | {{< mycode "red" "large" >}} |
{{ .Get 0 }}、{{ .Get 1 }} |
| 内部内容 | {{< mycode >}}内容{{< /mycode >}} |
{{ .Inner }} |
内置 Shortcodes
Hugo 内置了多个实用短代码,开箱即用:
{{< figure src="..." title="..." >}}— 带标题的图片{{< youtube VIDEO_ID >}}— 嵌入 YouTube{{< tweet TWEET_ID >}}— 嵌入推文{{< gist USERNAME GIST_ID >}}— 嵌入 Gist
Partials(局部模板)—— 模板层面的复用
Partials 是 layouts/partials/ 下的可复用模板片段,适合封装通用逻辑。
示例:可复用的 SEO 元数据 Partial
1 | <!-- layouts/partials/seo.html --> |
在模板中调用:
1 | <head> |
带缓存的 Partial(性能优化):
{{ partialCached "footer.html" . .Section }} |
自定义函数与数据文件
1. 使用 Hugo 内置函数
Hugo 提供了丰富的内置模板函数,如:
hugo.Version— 获取 Hugo 版本now.Format "2006-01-02"— 格式化日期resources.Get/resources.Match— 资源处理markdownify— Markdown 渲染
2. 数据文件扩展
在 data/ 目录放置 JSON/YAML/TOML 文件,通过 site.Data.文件名 在模板中访问,适合:
- 导航菜单配置
- 多语言字典
- 外部 API 数据缓存
参考资源
- Hugo 官方 Modules 文档 — 权威指南
- Hinode 模块开发指南 — 实战教程,包含完整的 KaTeX 模块开发案例
- HugoMods 模块集合 — 大量开源模块参考
Go Template
Hugo 的模板引擎基于 Go 的 text/template,但做了大量扩展。以下是核心语法和常用模式的速查指南:
1. 基础语法
所有模板逻辑都包裹在 {{ }} 中。
1 | {{/* 注释 */}} |
上下文(.):在模板中,. 代表当前数据上下文。在单页模板中它是 Page 对象;在列表页中它是当前遍历的元素。
2. 变量
1 | {{ $title := .Title }} // 定义变量 |
注意:Go Template 中变量作用域受
{{ if }}、{{ range }}、{{ with }}影响,内部赋值不会泄漏到外部。
3. 条件判断
1 | {{ if .Params.draft }} |
Truthy 判断:空字符串、0、空切片、nil 都为 false。
4. 循环(range)
1 | {{ range .Pages }} |
在 range 内部,. 变成了当前元素。如需访问外层上下文:
{{ $.Site.Title }} // $ 绑定最外层上下文 |
5. with(上下文切换)
with 在值存在时进入新作用域,并将 . 设为该值:
1 | {{ with .Params.featured_image }} |
6. 管道(Pipes)
Go Template 的管道类似 Unix 管道,从左到右传递:
1 | {{ .Summary | strings.ToUpper | htmlEscape }} |
7. 常用 Hugo 函数
1 | // ===== 内容 ===== |
8. 模板组织
1 | // 引入 Partial(组件) |
9. 作用域陷阱(常见错误)
1 | {{ $author := .Params.author }} // 外层定义 |
10. 调试技巧
1 | {{ printf "%#v" . }} // 打印完整对象结构 |
背景
我发现,在Markdown文章中使用 ![]() 时,为了让Hugo博客正常显示本地图片,本地图片路径必须以根相对路径(以/开头)开头。
为什么必须以 / 开头?
Hugo 构建后,所有内容最终都输出到 public/ 目录。浏览器访问你的站点时,/ 就是站点的根目录,也就是 public/ 文件夹。
如果路径不是以/开头,就要在路径前面添加/。
public 目录
1 | 你的项目/ |
开发
选型
决策链:
1 | 需要统一图片路径? |
在 Hugo 中,最优雅的做法是使用 Image Render Hook(渲染钩子)。Hugo 会在渲染 Markdown 的 ![]() 图片语法时,调用你自定义的模板,你可以在模板中修改图片路径后再输出 HTML。
Modules
在你的项目或主题中创建文件render-image.html:
1 | layouts/ |
写入以下内容:
1 | {{- $url := .Destination -}} |
路径转换效果
| Markdown 原文 | 最终输出 HTML |
|---|---|
 |
<img src="/./image.png" alt="图"> |
 |
<img src="/../assets/image.png" alt="图"> |
 |
<img src="/image.png" alt="图">(不变) |
 |
<img src="https://example.com/a.png" alt="图">(不变) |
代码解析
Hugo 的模板语法(Go Template)。它长得有点像 HTML 里插了一些特殊标签,思路是:拿到数据 → 判断条件 → 拼接字符串 → 输出 HTML。
基础:
{{- … -}}:这是 Hugo 的模板代码块,-表示去掉前后空白(让输出更干净)。{{- /* ... */ -}}:注释,Hugo 不会输出,给人看的
开头:存变量
1 | {{- $url := .Destination -}} |
.Destination:图片路径,如image.png或/images/a.jpg.PlainText:里的 alt 文本.Title:里的 title
{{- $url := .Destination -}}
$url :=:定义一个变量,名字叫$url。.Destination:从 Hugo 拿到图片的原始路径。
比如文章里写了,这里.Destination的值就是image.png。
1:外部链接
1 | {{- if or (strings.HasPrefix $url "http://") (strings.HasPrefix $url "https://") (strings.HasPrefix $url "//") -}} |
| 代码 | 意思 |
|---|---|
{{- /* ... */ -}} |
注释,Hugo 不会输出,给人看的 |
strings.HasPrefix $url "http://" |
检查是否以 http:// 开头 |
or(...) |
三个条件满足一个就行 |
safeURL |
告诉 Hugo 这个链接是安全的,不要转义 :// 等特殊符号 |
{{ with $alt }} |
如果有 alt 文字,才输出 alt="..." |
htmlEscape . |
把 alt 里的特殊字符(如 " < > &)转义成安全字符 |
如果是网上图片(http、https、// 开头),直接原样输出,什么都不改。
2:已以 / 开头的路径
1 | {{- else if strings.HasPrefix $url "/" -}} |
| 代码 | 意思 |
|---|---|
strings.TrimPrefix "/" $url |
去掉开头的 /。如 /images/a.jpg → images/a.jpg |
resources.Get ... |
去 Hugo 的 assets 文件夹 里找这张图片 |
{{- with $img -}} |
如果找到了,执行这块 |
.RelPermalink |
图片的最终 URL(Hugo 处理后的) |
.Width / .Height |
图片的宽高(Hugo 自动读取) |
{{- else -}} |
如果没找到,走下面这块 |
{{ $url | safeURL }} |
原样输出,就当它是个普通路径 |
如果地址已经是
/xxx开头,不用改,直接输出。
3 相对路径(不以 / 开头的)
1 | {{- else -}} |
| 代码 | 意思 |
|---|---|
{{- else -}} |
上面两个条件都不满足,走这里 |
printf "/%s" $url |
字符串拼接:/ + 原路径 |
$fixedUrl := |
把拼接结果存到新变量 $fixedUrl |
只要不是外部链接,也不是
/开头,就在前面加一个/。
直观感受
1 | 用户写:  |
1 | 用户写:  |
1 | 用户写:  |