跳到主要内容

渲染钩子

渲染钩子(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自定义表格渲染
Passthroughrender-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 "链接标题")
--- ------------ --------
文本 目标地址 标题(可选)

上下文变量

链接渲染钩子接收以下上下文变量:

变量类型说明
.PagePage包含链接的页面对象
.Destinationstring链接目标 URL
.Titlestring链接标题(Markdown 中的可选部分)
.Textstring链接文本(HTML 格式)
.PlainTextstring链接文本(纯文本格式)
.IsSafeboolURL 是否安全(非 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 图片语法

![图片描述](/images/photo.jpg "图片标题")
--- --------------- --------
描述 图片路径 标题(可选)

上下文变量

图片渲染钩子接收以下上下文变量:

变量类型说明
.PagePage包含图片的页面对象
.Destinationstring图片路径
.Titlestring图片标题
.Textstring图片描述(alt 文本)
.IsBlockbool是否为块级图片(Hugo 0.108+)
.Ordinalint页面内图片序号

处理页面资源

当图片位于页面包(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>

标题渲染钩子

上下文变量

变量类型说明
.Levelint标题级别(1-6)
.Textstring标题文本(HTML)
.PlainTextstring标题文本(纯文本)
.Anchorstring自动生成的锚点 ID
.Attributesmap标题属性
.PagePage包含标题的页面

添加锚点链接

{{- /* 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 }}>

引用块渲染钩子

上下文变量

变量类型说明
.Textstring引用内容(HTML)
.Attributesmap引用块属性
.PagePage包含引用块的页面

自定义引用块样式

{{- /* 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 渲染钩子接收以下上下文变量:

变量类型说明
.Innerstring分隔符内的原始内容
.Typestring元素类型:blockinline
.PagePage当前页面对象
.PageInnerPage嵌套页面引用
.Ordinalint页面内元素序号
.Positionstring元素在页面中的位置
.AttributesmapMarkdown 属性(仅块级元素)

创建 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 函数支持以下选项:

选项类型默认值说明
displayModeboolfalse是否为块级公式
outputstringmathml输出格式:mathmlhtmlhtmlAndMathml
errorColorstring#cc0000错误信息颜色
throwOnErrorbooltrue出错时是否抛出异常
macrosmap自定义宏定义
fleqnboolfalse是否左对齐公式,左边距 2em
minRuleThicknessfloat0.04分数线最小粗细(单位:em)
strictstringerror处理非官方支持 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 内容的情况下,实现丰富的自定义功能。