首页
/ Hugo PageInner 渲染钩子详解:RenderShortcodes 引入内容时如何正确解析相对链接与资源

Hugo PageInner 渲染钩子详解:RenderShortcodes 引入内容时如何正确解析相对链接与资源

2026-09-04 10:46:20作者:钟日瑜

本文围绕 Hugo 渲染钩子(Render Hook)体系中的 PageInner 上下文展开。它解决的核心问题是:当使用 .RenderShortcodes 把一个页面的 Markdown 内容引入到另一个页面时,内容中出现的相对链接和图片资源应当相对于"被引入的页面"而非"当前渲染的页面"来解析。读完后,你将掌握 include 短代码的完整写法、PagePageInner 在不同渲染钩子中的取值差异,以及 Hugo 通过页面 ID 栈(PID stack)在源码层面维护这一双层上下文的实现机制。

一、为什么需要 PageInner:两个上下文的问题

Hugo 的渲染钩子在 layouts/_markup/ 目录下以模板文件形式定义,处理标题、链接、图片、代码块等 Markdown 元素的最终 HTML 渲染。绝大多数钩子只需要知道"我正在渲染哪个页面",对应钩子数据中的 Page

但存在一类特殊场景:用短代码把一个页面的原始 Markdown 引入到另一个页面中渲染。典型做法是写一个 include 短代码,找到目标页面后调用它的 RenderShortcodes 方法,将内容"就地"渲染进当前文档。此时一次渲染过程里实际涉及两个页面:

  • 宿主页面(host page):正在被渲染的页面,短代码就定义在这里;
  • 被引入页面(included page):内容被拉进来渲染的那个页面,其中的链接、图片都是按它自己的目录写的。

如果没有区分这两者,被引入页面中 kitten 这类相对路径就会被错误地解析到宿主页面目录下,生成断链。PageInner 就是为被引入页面准备的上下文:

原文档给出的定位:PageInner 的首要用途,是将被引入的 Page 的链接和页面资源(page resources)相对于该页面进行解析。

同时,页脚(footnotes)和目录(table of contents)这类需要全局聚合的机制仍然基于宿主页面工作,不会因引入内容而割裂。

二、核心用例:include 短代码完整实现

Hugo 文档给出的标准写法是在 layouts/_shortcodes/include.html 中定义:

{{ with .Get 0 }}
  {{ with $.Page.GetPage . }}
    {{- .RenderShortcodes }}
  {{ else }}
    {{ errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }}
  {{ end }}
{{ else }}
  {{ errorf "The %q shortcode requires a positional parameter indicating the logical path of the file to include. See %s" .Name .Position }}
{{ end }}

要点说明:

  1. 位置参数.Get 0 取第一个位置参数,即被引入页面的逻辑路径(logical path),如 /posts/post-2
  2. $.Page.GetPage .:以宿主页面为基准查找目标页面。注意这里用的是 $.Page(短代码作用域中的 $ 即当前页面),保证查找的基准正确;
  3. .RenderShortcodes:这是整个机制的触发点——它不会把目标页面编译成完整 HTML 再嵌入,而是对其原始 Markdown 做一次"仅短代码"的渲染处理。被引入页面内部的短代码、以及渲染钩子上下文,都会在这一阶段生效;
  4. 错误处理:未找到页面或未传参时通过 errorf 抛出带有短代码名称与位置的明确报错,便于排查。

然后在任意 Markdown 内容中用 Markdown 记法调用该短代码(这一点后文会解释为何必须):

{{%/* include "/posts/post-2" */%}}

渲染完成后,如果在渲染 /posts/post-2 内容的过程中触发了任何渲染钩子(标题、链接、图片等),钩子拿到的上下文是:

钩子中调用的方法 返回值
Page /posts/post-1(宿主页面)
PageInner /posts/post-2(被引入页面)

原文档还明确了一条例外行为:PageInner 在"不相关"时回退为 Page 的取值,并且永远返回一个有效值。因此钩子模板中可以放心地直接调用 .PageInner,无需判空——在普通渲染(没有 RenderShortcodes 介入)时,它就等于 Page 本身。

三、关键限制:仅限 Markdown 记法调用

原文档中有一条重要注释,值得单独强调并附上仓库中的测试证据:

PageInner 方法只对调用了 RenderShortcodes 方法的短代码有意义,并且必须使用 Markdown 记法{{% ... %}})调用该短代码。

为什么 HTML 记法({{< ... >}})不行?因为 HTML 内容文件(.html)中的短代码是在模板渲染阶段(template render)被展开的,展开后的结果是纯字符串,不会再经过 Markdown 解析器处理,RenderShortcodes 产生的内容也就丢失了 Markdown 结构信息,PageInner 的解析机制无从谈起。

仓库集成测试 rendershortcodes_test.go 中的 TestRenderShortcodesNestedPageContextIssue12356 恰好覆盖了这个约束:同一份测试站点里,content/markdown/_index.md(Markdown 文件)和 content/html/_index.html(HTML 文件)都调用了同样的 include 短代码。断言结果:

  • Markdown 页面:kitten 被正确渲染为 Image: /markdown/pixel3.pnghugo_ctx 特殊上下文标记未被泄漏到输出(断言 ! hugo_ctx);
  • HTML 页面:内容保持原始文本形态(测试只断言 ! hugo_ctx,输出中不包含已解析的图片),印证了 HTML 记法下引入内容不会被二次解析。

四、源码纵深:PID 栈如何维护双层页面上下文

从源码结构看,Page/PageInner 的双层上下文由渲染上下文中一个"页面 ID 栈"(page ID stack,PID stack)支撑。

4.1 钩子接口定义

渲染钩子统一实现的上下文接口定义在 hooks.go

type PageProvider interface {
	// Page is the page being rendered.
	Page() any

	// PageInner may be different than Page when .RenderShortcodes is in play.
	// The main use case for this is to include other pages' markdown into the current page
	// but resolve resources and pages relative to the original.
	PageInner() any
}

接口注释与文档表述完全一致:只有当 .RenderShortcodes 介入时,PageInner 才可能与 Page 不同。

4.2 PID 的压栈、出栈与回退

具体实现位于 Goldmark 适配层的 context.go

  • 渲染上下文 Context 持有一个 pids []uint64 栈(L52-L59),提供 PushPidPeekPidPopPid 三个操作(L88-L110);
  • 核心取值函数 GetPageAndPageInnerL162-L174):
func GetPageAndPageInner(rctx *Context) (any, any) {
	p := rctx.DocumentContext().Document
	pid := rctx.PeekPid()
	if pid > 0 {
		if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
			if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
				return p, v
			}
		}
	}
	return p, p
}

这段代码完整对应文档中"回退到 Page 且总是有值"的语义:栈顶有有效的页面 ID 且能通过 DocumentLookup 找到对应文档时,返回 (宿主页面, 被引入页面);否则返回 (p, p),即 PageInner 退化为 Page

4.3 上下文的注入点

PID 的压栈/出栈发生在 hugocontext.go 中:Goldmark 解析出的内部 hugo_ctx 标记节点(RenderShortcodes 注入的内部边界标记)在遍历 AST 时被识别,进入该节点时 ctx.PushPid(hctx.Pid),离开时 ctx.PopPid()。这样,仅在被引入内容的 AST 区段内部,PageInner 才指向被引入页面,区段之外的所有节点仍然只有 Page

每个渲染钩子实例创建时都会调用 NewBaseContext,其中 pagepageInner 两个字段即来自 GetPageAndPageInner,最终通过 hookBase.Page() / hookBase.PageInner() 暴露给模板。这也解释了测试中反复出现的 ! hugo_ctx 断言:hugo_ctx 只是内部导航标记,绝不能泄漏到最终 HTML。

五、内置钩子的 PageInner 用法:链接与图片

文档特别指出:Hugo 内嵌的链接和图片渲染钩子正是使用 PageInner 来解析 Markdown 链接和图片的目标路径。以下完整展示这两个模板的实现。

5.1 内嵌链接钩子 render-link.html

{{- $u := urls.Parse .Destination -}}
{{- $href := $u.String -}}
{{- if strings.HasPrefix $u.String "#" -}}
	{{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment -}}
{{- else if and $href (not $u.IsAbs) -}}
	{{- $path := strings.TrimPrefix "./" $u.Path -}}
	{{- with or
		($.PageInner.GetPage $path)
		($.PageInner.Resources.Get $path)
		(resources.Get $path)
	-}}
		{{- $href = .RelPermalink -}}
		{{- with $u.RawQuery -}}
			{{- $href = printf "%s?%s" $href . -}}
		{{- end -}}
		{{- with $u.Fragment -}}
			{{- $href = printf "%s#%s" $href . -}}
		{{- end -}}
	{{- end -}}
{{- end -}}
<a href="{{ $href }}" {{- with .Title }} title="{{ . }}" {{- end }}>{{ .Text }}</a>

解析逻辑分三层:

  1. 纯锚点#fragment):直接拼接到 .PageInner.RelPermalink,保证被引入页面内的锚点链接落在正确文档内;
  2. 相对路径:依次尝试 PageInner.GetPage(按页面查找)、PageInner.Resources.Get(按页面资源查找)、resources.Get(站点级资源兜底),命中后取其 RelPermalink,并保留原始 URL 中的 query 与 fragment;
  3. 绝对路径$u.IsAbs):原样输出。

注意所有查找都以 PageInner 为基准,只有站点级资源是全局兜底——这正是"相对于被引入页面解析"的体现。

5.2 内嵌图片钩子 render-image.html

{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String -}}
{{- if not $u.IsAbs -}}
	{{- $path := strings.TrimPrefix "./" $u.Path -}}
	{{- with or (.PageInner.Resources.Get $path) (resources.Get $path) -}}
		{{- $src = .RelPermalink -}}
		{{- with $u.RawQuery -}}
			{{- $src = printf "%s?%s" $src . -}}
		{{- end -}}
		{{- with $u.Fragment -}}
			{{- $src = printf "%s#%s" $src . -}}
		{{- end -}}
	{{- end -}}
{{- end -}}
<img src="{{ $src }}" alt="{{ .PlainText }}"
	{{- with .Title }} title="{{ . }}" {{- end -}}
	{{- range $k, $v := .Attributes -}}
		{{- if $v -}}
		{{- printf " %s=%q" $k ($v | transform.HTMLEscape) | safeHTMLAttr -}}
		{{- end -}}
	{{- end -}}>

图片钩子逻辑相同但更简洁:非绝对路径时先查 .PageInner.Resources.Get $path,再兜底 resources.Get $path,命中即替换为 RelPermalink

六、集成测试验证:被引入页面的元素各自解析、全局 TOC 完整

仓库中 rendershortcodes_test.goTestRenderShortcodesNestedPageContextIssue12356 是最直接的端到端验证。测试站点结构:

  • 宿主页面 content/markdown/_index.md(标题 Markdown),内含 # H1,通过 include 短代码引入 /posts/p1,之后又有一张宿主自己的图片 kitten 和一个 go 代码块;
  • 被引入页面 content/posts/p1/index.md(标题 p1),内含标题 ## H2-p1、两张相对图片 pixel1.png/pixel2.png、一个相对链接 p2 和一个 bash 代码块;
  • 测试用自定义渲染钩子(render-image.htmlrender-link.htmlrender-heading.htmlrender-codeblock.html)直接输出 .PageInner.Title 与解析结果,把上下文差异显式化。

关键断言:

// 图片:被引入页的图解析到 /posts/p1/ 下,宿主页的图解析到 /markdown/ 下
"Image: /posts/p1/pixel1.png|", "Image: /posts/p1/pixel2.png|", "Image: /markdown/pixel3.png|"
// 链接:被引入页的 p2 解析为 /posts/p2/
"Link: /posts/p2/|"
// 代码块:PageInner 区分了两份内容
"CodeBlock: p1: bash|", "CodeBlock: Markdown: go|"
// 标题:宿主标题与引入标题各自的 PageInner 均正确
"Heading: Markdown: H1|", "Heading: p1: H2-p1|"
// 目录碎片:全局聚合,同时包含宿主的 h1 和引入内容的 h2-p1
"Fragments: [h1 h2-p1]|"

最后一项 Fragments: [h1 h2-p1] 恰好印证了文档开头"保留页脚与目录的全局上下文"这一设计目标:虽然元素级上下文(PageInner)切换到了被引入页面,但目录碎片依然按宿主页面统一收集,两个标题各归其位。

七、实践要点小结

  1. 适用边界PageInner 只在短代码调用 RenderShortcodes 的场景下与 Page 不同;其他渲染钩子直接使用 Page 即可,PageInner 在那些场景下会安全地回退为 Page
  2. 必须用 Markdown 记法{{% include ... %}})调用触发短代码,HTML 记法({{< include ... >}})不会激活该机制;
  3. 在自定义渲染钩子模板中,凡是涉及"解析相对路径"的逻辑(GetPageResources.Get、锚点 RelPermalink 拼接),都应优先使用 .PageInner 而不是 .Page,这与内置 render-link.htmlrender-image.html 的写法一致;
  4. 涉及全局聚合的机制(TOC、页脚、注释)无需关心 PageInner,它们自动基于宿主页面工作;
  5. 排查引入内容断链问题时,可参考 hugolib/rendershortcodes_test.go 中的测试用例,它把 Page/PageInner 在标题、链接、图片、代码块上的取值差异全部以可断言的形式固定了下来。

理解并正确使用 PageInner,是编写可组合 Hugo 内容架构(多文件组装单页、文档模块化复用)的关键前提:它让每个被引入的 Markdown 片段保持"路径自洽",同时宿主页面维持统一的全局渲染边界。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341