渲染钩子
渲染钩子(Render Hooks)是 Hugo 提供的模板覆盖机制,允许你自定义 Markdown 元素转换为 HTML 的方式。通过渲染钩子,你可以精确控制链接、图片、代码块、标题等元素的输出。
为什么需要渲染钩子?
默认情况下,Hugo 使用 Goldmark 解析器将 Markdown 转换为 HTML。这个过程是自动的,但有时我们需要更多的控制:
- 为外部链接添加特殊样式或图标
- 为图片添加懒加载、响应式处理
- 自定义代码块的显示方式(添加复制按钮、行号等)
- 为标题添加锚点链接
渲染钩子让你可以在不修改 Markdown 源文件的情况下,实现这些自定义需求。
渲染钩子类型
Hugo 支持以下类型的渲染钩子:
| 钩子类型 | 文件名 | 用途 |
|---|---|---|
| 链接 | render-link.html | 自定义链接渲染 |
| 图片 | render-image.html | 自定义图片渲染 |
| 代码块 | render-codeblock.html | 自定义代码块渲染 |
| 标题 | render-heading.html | 自定义标题渲染 |
| 引用块 | render-blockquote.html | 自定义引用块渲染 |
| 表格 | render-table.html | 自定义表格渲染 |
| Passthrough | render-passthrough.html | 自定义原始文本渲染(如数学公式) |
钩子文件位置
渲染钩子模板放在 layouts/_default/_markup/ 目录下:
layouts/
└── _default/
└── _markup/
├── render-link.html # 链接钩子
├── render-image.html # 图片钩子
├── render-codeblock.html # 代码块钩子(通用)
├── render-codeblock-go.html # Go 代码专用钩子
├── render-heading.html # 标题钩子
├── render-blockquote.html # 引用块钩子
├── render-table.html # 表格钩子
└── render-passthrough.html # Passthrough 钩子
链接渲染钩子
Markdown 链接语法
一个 Markdown 链接包含三个部分:
[链接文本](/path/to/page "链接标题")
--- ------------ --------
文本 目标地址 标题(可选)
上下文变量
链接渲染钩子接收以下上下文变量:
| 变量 | 类型 | 说明 |
|---|---|---|
.Page | Page | 包含链接的页面对象 |
.Destination | string | 链接目标 URL |
.Title | string | 链接标题(Markdown 中的可选部分) |
.Text | string | 链接文本(HTML 格式) |
.PlainText | string | 链接文本(纯文本格式) |
.IsSafe | bool | URL 是否安全(非 javascript: 等) |
区分内部链接和外部链接
这是链接钩子最常见的用途——为外部链接添加特殊处理:
{{- /* layouts/_default/_markup/render-link.html */ -}}
{{- $destination := .Destination -}}
{{- $title := .Title -}}
{{- $text := .Text -}}
{{- /* 判断是否为外部链接 */ -}}
{{- $isExternal := hasPrefix $destination "http" -}}
{{- if $isExternal -}}
{{- /* 外部链接:添加 target="_blank" 和安全属性 */ -}}
<a href="{{ $destination }}"
{{ with $title }}title="{{ . }}"{{ end }}
target="_blank"
rel="noopener noreferrer"
class="external-link">
{{ $text | safeHTML }}
<svg class="external-icon" viewBox="0 0 24 24" width="14" height="14">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
{{- else -}}
{{- /* 内部链接:正常渲染 */ -}}
{{- $url := urls.Parse $destination -}}
{{- if $url.Path -}}
{{- $fragment := "" -}}
{{- with $url.Fragment }}{{ $fragment = printf "#%s" . }}{{ end -}}
{{- with $url.Path }}
{{- /* 解析页面引用 */ -}}
{{- $page := .Page.GetPage . -}}
{{- if $page -}}
{{- $destination = printf "%s%s" $page.RelPermalink $fragment -}}
{{- end -}}
{{- end -}}
{{- end -}}
<a href="{{ $destination }}" {{ with $title }}title="{{ . }}"{{ end }}>
{{ $text | safeHTML }}
</a>
{{- end -}}
解析页面引用
当 Markdown 中使用相对路径引用其他页面时,需要正确解析:
{{- /* 解析内部页面链接 */ -}}
{{- $url := urls.Parse .Destination -}}
{{- $fragment := "" -}}
{{- with $url.Fragment }}{{ $fragment = printf "#%s" . }}{{ end -}}
{{- with $url.Path -}}
{{- /* 尝试获取页面 */ -}}
{{- $page := $.Page.GetPage . -}}
{{- with $page -}}
{{- /* 找到页面,使用其永久链接 */ -}}
<a href="{{ .RelPermalink }}{{ $fragment }}">
{{ $.Text | safeHTML }}
</a>
{{- else -}}
{{- /* 未找到页面,保持原样 */ -}}
<a href="{{ $.Destination }}">{{ $.Text | safeHTML }}</a>
{{- end -}}
{{- else -}}
{{- /* 纯锚点链接 */ -}}
<a href="{{ $fragment }}">{{ .Text | safeHTML }}</a>
{{- end -}}
完整链接钩子示例
{{- /* layouts/_default/_markup/render-link.html */ -}}
{{- $destination := .Destination | safeURL -}}
{{- $title := .Title -}}
{{- $text := .Text -}}
{{- $page := .Page -}}
{{- /* 检查是否为外部链接 */ -}}
{{- $isExternal := or
(hasPrefix $destination "http://")
(hasPrefix $destination "https://")
-}}
{{- if $isExternal -}}
{{- /* 外部链接处理 */ -}}
<a href="{{ $destination }}"
{{ with $title }}title="{{ . }}"{{ end }}
target="_blank"
rel="noopener noreferrer nofollow"
class="link-external">
{{ $text | safeHTML }}
<span class="sr-only">(opens in new tab)</span>
<svg aria-hidden="true" class="icon-external" width="12" height="12" viewBox="0 0 24 24">
<path fill="currentColor" d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"/>
</svg>
</a>
{{- else -}}
{{- /* 内部链接处理 */ -}}
{{- $url := urls.Parse $destination -}}
{{- $fragment := "" -}}
{{- with $url.Fragment }}{{ $fragment = printf "#%s" . }}{{ end -}}
{{- $resolvedPath := $url.Path -}}
{{- /* 解析页面引用 */ -}}
{{- with $url.Path -}}
{{- $resolvedPage := $page.GetPage . -}}
{{- with $resolvedPage -}}
{{- $resolvedPath = .RelPermalink -}}
{{- end -}}
{{- end -}}
<a href="{{ $resolvedPath }}{{ $fragment }}"
{{ with $title }}title="{{ . }}"{{ end }}
class="link-internal">
{{ $text | safeHTML }}
</a>
{{- end -}}
图片渲染钩子
Markdown 图片语法

--- --------------- --------
描述 图片路径 标题(可选)
上下文变量
图片渲染钩子接收以下上下文变量:
| 变量 | 类型 | 说明 |
|---|---|---|
.Page | Page | 包含图片的页面对象 |
.Destination | string | 图片路径 |
.Title | string | 图片标题 |
.Text | string | 图片描述(alt 文本) |
.IsBlock | bool | 是否为块级图片(Hugo 0.108+) |
.Ordinal | int | 页面内图片序号 |
处理页面资源
当图片位于页面包(Page Bundle)中时,可以获取图片的详细信息:
{{- /* layouts/_default/_markup/render-image.html */ -}}
{{- $destination := .Destination -}}
{{- $title := .Title -}}
{{- $alt := .Text -}}
{{- $page := .Page -}}
{{- /* 尝试获取页面资源 */ -}}
{{- $image := $page.Resources.GetMatch $destination -}}
{{- if $image -}}
{{- /* 页面资源:可以进行图片处理 */ -}}
{{- $small := $image.Resize "400x webp" -}}
{{- $medium := $image.Resize "800x webp" -}}
{{- $large := $image.Resize "1200x webp" -}}
<figure class="image-wrapper">
<picture>
<source media="(max-width: 400px)" srcset="{{ $small.RelPermalink }}">
<source media="(max-width: 800px)" srcset="{{ $medium.RelPermalink }}">
<img src="{{ $large.RelPermalink }}"
alt="{{ $alt }}"
{{ with $title }}title="{{ . }}"{{ end }}
width="{{ $large.Width }}"
height="{{ $large.Height }}"
loading="lazy"
decoding="async">
</picture>
{{ with $title }}
<figcaption>{{ . }}</figcaption>
{{ end }}
</figure>
{{- else -}}
{{- /* 非页面资源:直接渲染 */ -}}
<img src="{{ $destination }}"
alt="{{ $alt }}"
{{ with $title }}title="{{ . }}"{{ end }}
loading="lazy"
decoding="async">
{{- end -}}
添加懒加载和响应式处理
{{- /* layouts/_default/_markup/render-image.html */ -}}
{{- $destination := .Destination -}}
{{- $alt := .Text -}}
{{- $title := .Title -}}
{{- $page := .Page -}}
{{- $isBlock := .IsBlock | default false -}}
{{- /* 获取图片资源 */ -}}
{{- $image := $page.Resources.GetMatch $destination -}}
{{- if not $image -}}
{{- $image = resources.Get $destination -}}
{{- end -}}
{{- if $image -}}
{{- /* 生成不同尺寸 */ -}}
{{- $placeholder := $image.Resize "20x webp q20" -}}
{{- $srcset := slice -}}
{{- $widths := slice 320 640 960 1280 -}}
{{- range $widths -}}
{{- if le . $image.Width -}}
{{- $resized := $image.Resize (printf "%dx webp" .) -}}
{{- $srcset = $srcset | append (printf "%s %dw" $resized.RelPermalink .) -}}
{{- end -}}
{{- end -}}
<figure class="image {{ if $isBlock }}block-image{{ else }}inline-image{{ end }}">
<img src="{{ $image.RelPermalink }}"
srcset="{{ delimit $srcset ", " }}"
sizes="(max-width: 768px) 100vw, 768px"
alt="{{ $alt }}"
{{ with $title }}title="{{ . }}"{{ end }}
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
decoding="async"
style="background-image: url({{ $placeholder.RelPermalink }}); background-size: cover;">
{{ with $title }}
<figcaption>{{ . }}</figcaption>
{{ end }}
</figure>
{{- else -}}
{{- /* 找不到图片资源 */ -}}
<img src="{{ $destination }}"
alt="{{ $alt }}"
{{ with $title }}title="{{ . }}"{{ end }}
loading="lazy"
decoding="async">
{{- end -}}
代码块渲染钩子
Markdown 代码块语法
```python {linenos=true, hl_lines=[2,3]}
def hello():
print("Hello") # 高亮
print("World") # 高亮
### 上下文变量
代码块渲染钩子接收以下上下文变量:
| 变量 | 类型 | 说明 |
|------|------|------|
| `.Lang` | string | 代码语言(如 python, go) |
| `.Type` | string | 同 `.Lang` |
| `.Code` | string | 原始代码内容 |
| `.Inner` | string | 同 `.Code` |
| `.Ordinal` | int | 页面内代码块序号 |
| `.Page` | Page | 包含代码块的页面 |
| `.Attributes` | map | 代码块属性 |
| `.Options` | map | 解析后的高亮选项 |
| `.Position` | object | 源码位置信息 |
### 基础代码块钩子
```html
{{- /* layouts/_default/_markup/render-codeblock.html */ -}}
{{- $lang := .Lang | default "text" -}}
{{- $code := .Code -}}
{{- $attrs := .Attributes -}}
{{- /* 解析属性 */ -}}
{{- $linenos := $attrs.linenos | default false -}}
{{- $hlLines := $attrs.hl_lines | default slice -}}
{{- $title := $attrs.title | default "" -}}
{{- /* 构建高亮选项 */ -}}
{{- $options := dict
"linenos" $linenos
"hl_lines" $hlLines
"linenostart" ($attrs.linenostart | default 1)
-}}
{{- /* 应用语法高亮 */ -}}
{{- $highlighted := highlight $code $lang $options -}}
<figure class="code-block" data-lang="{{ $lang }}">
{{ with $title }}
<figcaption class="code-title">{{ . }}</figcaption>
{{ end }}
<div class="code-content">
{{ $highlighted | safeHTML }}
</div>
<button class="copy-button" data-code="{{ $code | plainify | htmlEscape }}">
复制代码
</button>
</figure>
带行号和复制按钮
{{- /* layouts/_default/_markup/render-codeblock.html */ -}}
{{- $lang := .Lang | default "text" -}}
{{- $code := .Code -}}
{{- $attrs := .Attributes -}}
{{- $ordinal := .Ordinal -}}
{{- /* 默认配置 */ -}}
{{- $linenos := $attrs.linenos | default true -}}
{{- $hlLines := $attrs.hl_lines | default slice -}}
{{- $linenostart := $attrs.linenostart | default 1 -}}
{{- $title := $attrs.title | default "" -}}
{{- $filename := $attrs.file | default "" -}}
{{- /* 构建选项 */ -}}
{{- $options := dict
"linenos" $linenos
"hl_lines" $hlLines
"linenostart" $linenostart
"anchorLineNos" true
"lineAnchors" (printf "code-%d" $ordinal)
-}}
{{- $highlighted := highlight $code $lang $options -}}
{{- $lineCount := split $code "\n" | len -}}
<figure class="code-block" data-lang="{{ $lang }}" data-lines="{{ $lineCount }}">
<header class="code-header">
<span class="code-lang">{{ $lang }}</span>
{{ with $filename }}<span class="code-filename">{{ . }}</span>{{ end }}
<button class="copy-btn" type="button" aria-label="复制代码" data-target="code-{{ $ordinal }}">
<svg class="icon" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/>
</svg>
<span class="label">复制</span>
</button>
</header>
<div class="code-container" id="code-{{ $ordinal }}">
{{ $highlighted | safeHTML }}
</div>
{{ with $title }}
<footer class="code-footer">{{ . }}</footer>
{{ end }}
</figure>
特定语言的专用钩子
为特定语言创建独立模板,如 render-codeblock-bash.html:
{{- /* layouts/_default/_markup/render-codeblock-bash.html */ -}}
<div class="terminal">
<header class="terminal-header">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="terminal-title">Terminal</span>
</header>
<div class="terminal-body">
<pre><code class="language-bash">{{ .Code }}</code></pre>
</div>
</div>
标题渲染钩子
上下文变量
| 变量 | 类型 | 说明 |
|---|---|---|
.Level | int | 标题级别(1-6) |
.Text | string | 标题文本(HTML) |
.PlainText | string | 标题文本(纯文本) |
.Anchor | string | 自动生成的锚点 ID |
.Attributes | map | 标题属性 |
.Page | Page | 包含标题的页面 |
添加锚点链接
{{- /* layouts/_default/_markup/render-heading.html */ -}}
{{- $level := .Level -}}
{{- $text := .Text -}}
{{- $anchor := .Anchor -}}
{{- $attrs := .Attributes -}}
<h{{ $level }} id="{{ $anchor }}" {{ range $k, $v := $attrs }}{{ printf "%s=%q " $k $v | safeHTMLAttr }}{{ end }}>
<a href="#{{ $anchor }}" class="heading-anchor" aria-label="链接到标题">
<svg class="icon" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.64L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z"/>
</svg>
</a>
{{ $text | safeHTML }}
</h{{ $level }}>
引用块渲染钩子
上下文变量
| 变量 | 类型 | 说明 |
|---|---|---|
.Text | string | 引用内容(HTML) |
.Attributes | map | 引用块属性 |
.Page | Page | 包含引用块的页面 |
自定义引用块样式
{{- /* layouts/_default/_markup/render-blockquote.html */ -}}
{{- $text := .Text -}}
{{- $attrs := .Attributes -}}
{{- $author := $attrs.author | default "" -}}
<blockquote {{ range $k, $v := $attrs }}{{ printf "%s=%q " $k $v | safeHTMLAttr }}{{ end }}>
<div class="quote-content">
{{ $text | safeHTML }}
</div>
{{ with $author }}
<footer class="quote-author">—— {{ . }}</footer>
{{ end }}
</blockquote>
在 Markdown 中使用:
> 这是一段引用文字。
> 人生就像一盒巧克力。
{author="阿甘正传"}
配套 CSS 样式
链接样式
/* 外部链接 */
.link-external {
color: #2563eb;
text-decoration: underline;
}
.link-external:hover {
color: #1d4ed8;
}
.link-external .icon-external {
margin-left: 0.25rem;
vertical-align: middle;
}
/* 内部链接 */
.link-internal {
color: #3b82f6;
text-decoration: none;
}
.link-internal:hover {
text-decoration: underline;
}
/* 屏幕阅读器专用文本 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
图片样式
.image-wrapper {
margin: 1.5rem 0;
text-align: center;
}
.image-wrapper img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.image-wrapper figcaption {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
font-style: italic;
}
/* 行内图片 */
.inline-image {
display: inline-block;
margin: 0 0.25rem;
}
/* 块级图片 */
.block-image {
display: block;
margin: 1.5rem auto;
}
代码块样式
.code-block {
margin: 1.5rem 0;
border-radius: 8px;
overflow: hidden;
background: #1e1e1e;
}
.code-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 1rem;
background: #2d2d2d;
border-bottom: 1px solid #404040;
}
.code-lang {
font-size: 0.75rem;
font-weight: 600;
color: #60a5fa;
text-transform: uppercase;
}
.code-filename {
font-size: 0.875rem;
color: #9ca3af;
}
.copy-btn {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: transparent;
border: 1px solid #4b5563;
border-radius: 4px;
color: #9ca3af;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover {
background: #374151;
color: #e5e7eb;
}
.code-container {
overflow-x: auto;
}
.code-footer {
padding: 0.5rem 1rem;
background: #2d2d2d;
border-top: 1px solid #404040;
font-size: 0.75rem;
color: #9ca3af;
}
/* 终端样式 */
.terminal {
background: #1a1a2e;
border-radius: 8px;
overflow: hidden;
}
.terminal-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: #16213e;
}
.terminal-header .dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.terminal-header .dot.red { background: #ff5f56; }
.terminal-header .dot.yellow { background: #ffbd2e; }
.terminal-header .dot.green { background: #27c93f; }
.terminal-title {
margin-left: auto;
font-size: 0.75rem;
color: #9ca3af;
}
.terminal-body pre {
margin: 0;
padding: 1rem;
color: #e5e7eb;
}
标题锚点样式
.heading-anchor {
opacity: 0;
margin-left: 0.5rem;
color: #6b7280;
text-decoration: none;
transition: opacity 0.2s;
}
h1:hover .heading-anchor,
h2:hover .heading-anchor,
h3:hover .heading-anchor,
h4:hover .heading-anchor,
h5:hover .heading-anchor,
h6:hover .heading-anchor {
opacity: 1;
}
.heading-anchor:hover {
color: #3b82f6;
}
JavaScript 增强
复制按钮功能
// 复制代码功能
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const figure = btn.closest('.code-block');
const code = figure.querySelector('code').textContent;
const label = btn.querySelector('.label');
try {
await navigator.clipboard.writeText(code);
label.textContent = '已复制';
btn.classList.add('copied');
setTimeout(() => {
label.textContent = '复制';
btn.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('复制失败:', err);
label.textContent = '复制失败';
}
});
});
外部链接确认
// 外部链接点击确认
document.querySelectorAll('a[target="_blank"]').forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
// 可以添加确认逻辑或统计
console.log('Opening external link:', href);
});
});
图表渲染钩子
Hugo 支持在 Markdown 中嵌入各种类型的图表。通过代码块渲染钩子,可以实现 Mermaid、GoAT 等图表的渲染。
GoAT 图表(ASCII 艺术)
GoAT(Go ASCII Art)是 Hugo 内置支持的 ASCII 图表格式。Hugo 提供了默认的渲染钩子,无需额外配置。
使用方式:
```goat
. . . .--- 1 .-- 1 / 1
/ \ | | .---+ .-+ +
/ \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2
+ + | | | | ---+ ---+ +
/ \ / \ .-+-. .-+-. .+. .+. | .--- 3 | .-- 3 \ / 3
/ \ / \ | | | | | | | | '---+ '-+ +
1 2 3 4 1 2 3 4 1 2 3 4 '--- 4 '-- 4 \ 4
**常用 GoAT 图表示例**:
**流程图**:
```markdown
```goat
+----------------+
| 开始 |
+-------+--------+
|
v
+-------+--------+
| 处理数据 |
+-------+--------+
|
v
+-------+--------+
| 结束 |
+----------------+
**文件树结构**:
```markdown
```goat
project/
├── src/
│ ├── index.js
│ └── utils.js
├── public/
│ └── index.html
└── package.json
**序列图**:
```markdown
```goat
Alice -> Bob: Hello
Bob --> Alice: Hi!
Alice -> Bob: How are you?
Bob --> Alice: Fine, thanks!
### Mermaid 图表
Mermaid 是更强大的图表库,支持流程图、序列图、甘特图、饼图等多种类型。Hugo 需要通过自定义渲染钩子来支持 Mermaid。
**创建 Mermaid 渲染钩子**:
`layouts/_default/_markup/render-codeblock-mermaid.html`:
```html
<pre class="mermaid">
{{ .Inner | htmlEscape | safeHTML }}
</pre>
{{ .Page.Store.Set "hasMermaid" true }}
在基础模板中加载 Mermaid 库:
在 layouts/_default/baseof.html 的 </body> 标签前添加:
{{ if .Store.Get "hasMermaid" }}
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
theme: 'default'
});
</script>
{{ end }}
这样只有当页面包含 Mermaid 图表时才会加载库文件,避免不必要的资源加载。
Mermaid 图表示例:
流程图:
```mermaid
flowchart TD
A[开始] --> B{是否登录?}
B -->|是| C[显示内容]
B -->|否| D[跳转登录]
D --> E[输入账号密码]
E --> F{验证通过?}
F -->|是| C
F -->|否| D
**序列图**:
```markdown
```mermaid
sequenceDiagram
participant 用户
participant 前端
participant 后端
participant 数据库
用户->>前端: 点击登录
前端->>后端: POST /api/login
后端->>数据库: 查询用户
数据库-->>后端: 返回用户信息
后端-->>前端: 返回 Token
前端-->>用户: 登录成功
**甘特图**:
```markdown
```mermaid
gantt
title 项目开发计划
dateFormat YYYY-MM-DD
section 设计阶段
需求分析 :a1, 2024-01-01, 7d
原型设计 :a2, after a1, 5d
section 开发阶段
前端开发 :b1, after a2, 14d
后端开发 :b2, after a2, 14d
测试 :b3, after b1, 7d
**类图**:
```markdown
```mermaid
classDiagram
class User {
+String name
+String email
+login()
+logout()
}
class Post {
+String title
+String content
+publish()
}
User "1" --> "*" Post : writes
**饼图**:
```markdown
```mermaid
pie showData
title 浏览器市场份额
"Chrome" : 65
"Safari" : 19
"Firefox" : 8
"Edge" : 5
"其他" : 3
### 高级 Mermaid 配置
**自定义主题**:
```html
{{ if .Store.Get "hasMermaid" }}
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
themeVariables: {
primaryColor: '#3b82f6',
primaryTextColor: '#fff',
primaryBorderColor: '#2563eb',
lineColor: '#64748b',
secondaryColor: '#1e293b',
tertiaryColor: '#334155'
}
});
</script>
{{ end }}
本地化 Mermaid 库:
将 Mermaid 库下载到 static/js/ 目录:
{{ if .Store.Get "hasMermaid" }}
<script src="/js/mermaid.min.js"></script>
<script>
mermaid.initialize({ startOnLoad: true });
</script>
{{ end }}
其他图表类型
PlantUML 支持:
layouts/_default/_markup/render-codeblock-plantuml.html:
{{ $encoded := .Inner | urlquery }}
{{ $url := printf "https://www.plantuml.com/plantuml/svg/~h%s" $encoded }}
<img src="{{ $url }}" alt="PlantUML Diagram">
D2 图表支持:
layouts/_default/_markup/render-codeblock-d2.html:
<div class="d2-diagram" data-code="{{ .Inner | base64Encode }}">
<pre>{{ .Inner }}</pre>
</div>
需要配合 JavaScript 库在客户端渲染。
图表样式建议
/* Mermaid 容器样式 */
.mermaid {
display: flex;
justify-content: center;
margin: 1.5rem 0;
padding: 1rem;
background: #f8fafc;
border-radius: 8px;
overflow-x: auto;
}
/* GoAT 图表样式 */
.goat-diagram {
font-family: monospace;
white-space: pre;
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.mermaid {
background: #1e293b;
}
}
最佳实践
1. 保持向后兼容
确保渲染钩子在资源不存在时仍然能正常工作:
{{- $image := .Page.Resources.GetMatch .Destination -}}
{{- if $image -}}
{{- /* 处理图片 */ -}}
{{- else -}}
{{- /* 降级处理 */ -}}
<img src="{{ .Destination }}" alt="{{ .Text }}">
{{- end -}}
2. 使用语义化 HTML
使用 <figure>、<figcaption> 等语义化标签:
<figure>
<img src="..." alt="...">
<figcaption>图片说明</figcaption>
</figure>
3. 添加无障碍支持
- 为图片提供
alt属性 - 为外部链接添加提示文字
- 为按钮添加
aria-label
4. 性能优化
- 为图片添加
loading="lazy" - 为图片添加
decoding="async" - 使用响应式图片减少不必要的带宽消耗
Passthrough 渲染钩子
Passthrough 渲染钩子是 Hugo 提供的特殊钩子,用于捕获和处理 Goldmark Passthrough 扩展保留的原始文本。最常见的用途是渲染数学公式。
什么是 Passthrough?
Goldmark 的 Passthrough 扩展会捕获 Markdown 中被特定分隔符包围的原始文本,并将其原样传递给渲染钩子处理。这意味着这些内容不会被 Markdown 解析器处理,而是保留原始格式。
Passthrough 元素分为两种类型:
- 块级元素(Block):独立成行的内容,如数学公式块
- 行内元素(Inline):嵌入在段落中的内容,如行内公式
配置 Passthrough 扩展
首先需要在 hugo.toml 中启用 Passthrough 扩展并定义分隔符:
[markup]
[markup.goldmark]
[markup.goldmark.extensions]
[markup.goldmark.extensions.passthrough]
enable = true
[markup.goldmark.extensions.passthrough.delimiters]
block = [['\[', '\]'], ['$$', '$$']]
inline = [['\(', '\)']]
配置说明:
block:块级元素的分隔符对,支持多组配置inline:行内元素的分隔符对
上下文变量
Passthrough 渲染钩子接收以下上下文变量:
| 变量 | 类型 | 说明 |
|---|---|---|
.Inner | string | 分隔符内的原始内容 |
.Type | string | 元素类型:block 或 inline |
.Page | Page | 当前页面对象 |
.PageInner | Page | 嵌套页面引用 |
.Ordinal | int | 页面内元素序号 |
.Position | string | 元素在页面中的位置 |
.Attributes | map | Markdown 属性(仅块级元素) |
创建 Passthrough 渲染钩子
最常见的用途是服务端渲染数学公式。使用 Hugo 内置的 transform.ToMath 函数:
layouts/_default/_markup/render-passthrough.html:
{{- $opts := dict
"output" "htmlAndMathml"
"displayMode" (eq .Type "block")
-}}
{{- with try (transform.ToMath .Inner $opts) }}
{{- with .Err }}
{{- errorf "无法渲染数学公式: %s (位置: %s)" . $.Position }}
{{- else }}
{{- .Value }}
{{- $.Page.Store.Set "hasMath" true }}
{{- end }}
{{- end -}}
在基础模板中加载 CSS
由于数学公式需要 KaTeX CSS 才能正确显示,需要在基础模板中条件加载:
<head>
{{/* 触发内容渲染,确保 hasMath 标志被正确设置 */}}
{{ $noop := .WordCount }}
{{ if .Page.Store.Get "hasMath" }}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
integrity="sha384-5TcZemv2ilvnYD6lYv69SkMzZaYLR6hG6Dp1Y9oN6a3aM1bD6tF3V7aM7hG6e3hP"
crossorigin="anonymous">
{{ end }}
</head>
重要提示:$noop := .WordCount 这行代码很关键,它强制 Hugo 在检查 hasMath 标志之前先渲染页面内容,确保标志被正确设置。
分离块级和行内模板
也可以为不同类型创建独立的模板:
layouts/
└── _markup/
├── render-passthrough-block.html # 块级公式
└── render-passthrough-inline.html # 行内公式
块级公式模板 layouts/_default/_markup/render-passthrough-block.html:
{{- $opts := dict "output" "htmlAndMathml" "displayMode" true -}}
{{- with try (transform.ToMath .Inner $opts) }}
{{- with .Err }}
{{- errorf "数学公式渲染错误: %s" . }}
{{- else }}
<div class="math-display">
{{- .Value }}
</div>
{{- $.Page.Store.Set "hasMath" true }}
{{- end }}
{{- end -}}
行内公式模板 layouts/_default/_markup/render-passthrough-inline.html:
{{- $opts := dict "output" "htmlAndMathml" "displayMode" false -}}
{{- with try (transform.ToMath .Inner $opts) }}
{{- with .Err }}
{{- errorf "数学公式渲染错误: %s" . }}
{{- else }}
<span class="math-inline">{{- .Value }}</span>
{{- $.Page.Store.Set "hasMath" true }}
{{- end }}
{{- end -}}
在 Markdown 中使用
配置完成后,可以在 Markdown 中直接使用数学公式:
行内公式:
爱因斯坦质能方程 \(E = mc^2\) 揭示了质量和能量的关系。
块级公式:
高斯积分公式:
\[
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
\]
或使用双美元符号:
$$
\sum_{i=1}^{n} i = \frac{n(n+1)}{2}
$$
transform.ToMath 选项
transform.ToMath 函数支持以下选项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
displayMode | bool | false | 是否为块级公式 |
output | string | mathml | 输出格式:mathml、html、htmlAndMathml |
errorColor | string | #cc0000 | 错误信息颜色 |
throwOnError | bool | true | 出错时是否抛出异常 |
macros | map | 自定义宏定义 | |
fleqn | bool | false | 是否左对齐公式,左边距 2em |
minRuleThickness | float | 0.04 | 分数线最小粗细(单位:em) |
strict | string | error | 处理非官方支持 LaTeX 特性的方式:error(报错)、ignore(忽略)、warn(警告)。v0.147.6+ |
自定义宏示例
定义常用的数学宏:
{{- $macros := dict
"\\R" "\\mathbb{R}"
"\\N" "\\mathbb{N}"
"\\Z" "\\mathbb{Z}"
"\\norm" "\\left\\| #1 \\right\\|"
"\\abs" "\\left| #1 \\right|"
-}}
{{- $opts := dict
"output" "htmlAndMathml"
"macros" $macros
"displayMode" (eq .Type "block")
-}}
{{- with try (transform.ToMath .Inner $opts) }}
{{- with .Err }}
{{- errorf "数学公式错误: %s" . }}
{{- else }}
{{- .Value }}
{{- $.Page.Store.Set "hasMath" true }}
{{- end }}
{{- end -}}
使用自定义宏:
实数集 \(\R\)、自然数集 \(\N\) 和整数集 \(\Z\) 是常用的数集。
向量的范数定义为 \(\norm{\mathbf{x}} = \sqrt{\sum_{i=1}^{n} x_i^2}\)。
PageInner 的用途
PageInner 主要用于处理嵌套内容时的页面引用。例如,创建一个 include 短代码来包含其他页面的内容:
{{/* layouts/shortcodes/include.html */}}
{{ with .Get 0 }}
{{ with $.Page.GetPage . }}
{{- .RenderShortcodes }}
{{ else }}
{{ errorf "找不到页面: %s" . }}
{{ end }}
{{ else }}
{{ errorf "include 短代码需要页面路径参数" }}
{{ end }}
当被包含的页面触发渲染钩子时,PageInner 会返回被包含的页面,而 Page 返回主页面。这对于正确解析相对链接和资源路径很重要。
与客户端渲染的对比
服务端渲染(使用 transform.ToMath)与客户端渲染(使用 MathJax/KaTeX JavaScript)的主要区别:
| 特性 | 服务端渲染 | 客户端渲染 |
|---|---|---|
| 页面加载速度 | 更快(无 JS 执行) | 较慢(需加载和执行 JS) |
| SEO 友好 | 是(HTML 输出) | 否(动态生成) |
| 离线支持 | 完全支持 | 依赖 JS 库 |
| 复杂公式 | 可能有限制 | 更完整支持 |
| 输出大小 | HTML/MathML | 依赖 JS 库大小 |
对于大多数数学公式,服务端渲染是推荐的选择,因为它提供更好的性能和用户体验。
小结
渲染钩子是 Hugo 中强大的自定义工具:
- 链接钩子:区分内外链接,添加特殊处理
- 图片钩子:响应式处理、懒加载、格式转换
- 代码块钩子:行号、高亮、复制按钮
- 标题钩子:锚点链接、目录生成
- 引用块钩子:自定义样式、作者信息
- Passthrough 钩子:服务端渲染数学公式,无需客户端 JavaScript
合理使用渲染钩子可以在不修改 Markdown 内容的情况下,实现丰富的自定义功能。