模板

短代码模板

创建自定义短代码模板:位置与命名、参数读取、返回值处理与嵌套。

简介

Hugo 为许多常见任务提供了内置短代码,但更专门的需求往往需要自己编写。常见的自定义短代码包括音频播放器、视频播放器、图片画廊、图表、地图、表格,以及各种自定义元素。

目录结构

短代码模板创建在 layouts/_shortcodes 目录中,可以放在该目录根部,也可以组织成子目录:

layouts/
└── _shortcodes/
    ├── diagrams/
    │   ├── kroki.html
    │   └── plotly.html
    ├── media/
    │   ├── audio.html
    │   ├── gallery.html
    │   └── video.html
    ├── capture.html
    ├── column.html
    ├── include.html
    └── row.html

在内容中调用子目录里的短代码时,写出它相对于 _shortcodes 目录的路径,并省略文件扩展名:

{{< media/audio path=/audio/podcast/episode-42.mp3 >}}

查找顺序

Hugo 依据短代码名称、当前输出格式与当前语言来选择模板。下例按具体程度从高到低排列,最笼统的排在最后:

短代码名 输出格式 语言 模板路径
foo html en layouts/_shortcodes/foo.en.html
foo html en layouts/_shortcodes/foo.html.html
foo html en layouts/_shortcodes/foo.html
foo html en layouts/_shortcodes/foo.html.en.html

输出格式为 rss 时同理,从 foo.en.rss.xml 依次退到 foo.rss.xml、foo.en.xml,最后是 foo.xml。

常用方法

短代码模板中有若干方法可用,layouts/_shortcodes 下各方法的作用如下:

方法 作用
.Get 按名称或序号读取单个参数。
.GetMatch 按模式匹配参数名并返回值。
.Params 以映射或切片的形式给出全部参数。
.IsNamedParams 判断本次调用使用的是命名参数还是位置参数。
.Inner 返回成对短代码之间的内容。
.InnerDeindent 返回去掉公共前导缩进后的内部内容。
.Parent 返回父级短代码的上下文,用于嵌套。
.Name 返回短代码名称,常用于错误信息。
.Position 返回短代码在内容文件中的位置,常用于错误信息。
.Page 返回调用该短代码的页面对象。

示例

下面的示例由浅入深,部分为便于理解做了简化。

插入年份

创建一个插入当前年份的短代码:

layouts/_shortcodes/year.html
{{- now.Format "2006" -}}

然后在内容中调用它:

content/example.md
This is {{< year >}}, and look at how far we've come.

这个短代码既可以行内使用,也可以独占一行作为块使用。若可能被行内调用,请用带连字符的动作定界符去掉两侧的空白。

插入图片

假设 content/example/index.md 是一个包含若干页面资源的页面包:

content/
├── example/
│   ├── a.jpg
│   └── index.md
└── _index.md

创建一个短代码,把图片取为页面资源、按给定宽度缩放、转换为 WebP 格式,并加上 alt 属性:

layouts/_shortcodes/image.html
{{- with .Page.Resources.Get (.Get "path") }}
  {{- with .Process (printf "resize %dx wepb" ($.Get "width")) -}}
    <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="{{ $.Get "alt" }}">
  {{- end }}
{{- end -}}

在内容中调用:

content/example/index.md
{{< image path=a.jpg width=300 alt="A white kitten" >}}

上面的写法用到了 with 语句在每次操作成功后重新绑定上下文、Get 方法按名称取出参数,以及 $ 访问模板最外层的上下文。

加上错误处理

前面的例子虽然可用,但图片不存在时会静默失败,缺少必需参数时也不会优雅退出。下面补上错误处理:

layouts/_shortcodes/image.html
{{- with .Get "path" }}
  {{- with $r := $.Page.Resources.Get ($.Get "path") }}
    {{- with $.Get "width" }}
      {{- with $r.Process (printf "resize %dx wepb" ($.Get "width" )) }}
        {{- $alt := or ($.Get "alt") "" -}}
        <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="{{ $alt }}">
      {{- end }}
    {{- else }}
      {{- errorf "The %q shortcode requires a 'width' argument: see %s" $.Name $.Position }}
    {{- end }}
  {{- else }}
    {{- warnf "The %q shortcode was unable to find %s: see %s" $.Name ($.Get "path") $.Position }}
  {{- end }}
{{- else }}
  {{- errorf "The %q shortcode requires a 'path' argument: see %s" .Name .Position }}
{{- end -}}

作者没有提供 path 或 width 时,这个模板会报错并让构建优雅失败;找不到指定路径的图片时给出警告;没有提供 alt 时,alt 属性被设为空字符串。Name 与 Position 方法能为错误与警告提供有用的上下文,例如缺少 width 参数时会抛出:

ERROR The "image" shortcode requires a 'width' argument: see "/home/user/project/content/example/index.md:7:1"

位置参数

短代码参数可以是命名参数,也可以是位置参数。前面用的是命名参数,下面看位置参数的写法。命名参数版本如下:

content/example/index.md
{{< image path=a.jpg width=300 alt="A white kitten" >}}

改用位置参数调用:

content/example/index.md
{{< image a.jpg 300 "A white kitten" >}}

在模板中用从 0 开始的下标配合 Get 方法取值,并把它们赋给含义清晰的变量:

layouts/_shortcodes/image.html
{{ $path := .Get 0 }}
{{ $width := .Get 1 }}
{{ $alt := .Get 2 }}

同时支持两种参数

可以让短代码同时接受命名参数与位置参数,但一次调用中不能混用。用 IsNamedParams 方法判断本次调用用的是哪一种:

layouts/_shortcodes/image.html
{{ $path := cond (.IsNamedParams) (.Get "path") (.Get 0) }}
{{ $width := cond (.IsNamedParams) (.Get "width") (.Get 1) }}
{{ $alt := cond (.IsNamedParams) (.Get "alt") (.Get 2) }}

这里用 cond(compare.Conditional 的别名)来实现:IsNamedParams 为真时按名称取值,否则按位置取值。

参数集合

用 Params 方法把参数作为一个集合取出。使用命名参数时它返回映射:

layouts/_shortcodes/image.html
{{ .Params.path }} → a.jpg
{{ .Params.width }} → 300
{{ .Params.alt }} → A white kitten

使用位置参数时它返回切片,需要用 index 按下标取值:

layouts/_shortcodes/image.html
{{ index .Params 0 }} → a.jpg
{{ index .Params 1 }} → 300
{{ index .Params 2 }} → A white kitten

把 Params 与 collections.IsSet 函数配合使用,可以判断某个参数是否被设置过,即使它的值是假值。

内部内容

用 Inner 方法取出短代码标签之间包裹的内容。下例同时把内容与标题传给短代码,短代码生成一个 div,其中包含显示标题的 h2 与传入的内容:

content/example.md
{{< contrived title="A Contrived Example" >}}
This is a **bold** word, and this is an _emphasized_ word.
{{< /contrived  >}}
layouts/_shortcodes/contrived.html
<div class="contrived">
  <h2>{{ .Get "title" }}</h2>
  {{ .Inner | .Page.RenderString }}
</div>

上面的调用使用标准写法,因此需要用 RenderString 方法把内部内容里的 Markdown 转换成 HTML。若改用 Markdown 写法调用,这一转换就是多余的。

嵌套

当短代码在另一个短代码内部被调用时,Parent 方法提供父级短代码的上下文,从而形成一种继承模型。下面这个例子虽属刻意构造,但足以说明概念。

假设有一个 gallery 短代码,接受一个名为 class 的参数:

layouts/_shortcodes/gallery.html
<div class="{{ .Get "class" }}">
  {{ .Inner }}
</div>

另有一个 img 短代码,接受一个名为 src 的参数;你希望它既能用在 gallery 内部,也能单独使用,由父级决定它的上下文:

layouts/_shortcodes/img.html
{{ $src := .Get "src" }}
{{ with .Parent }}
  <img src="{{ $src }}" class="{{ .Get "class" }}-image">
{{ else }}
  <img src="{{ $src }}">
{{ end }}

在内容中这样调用:

content/example.md
{{< gallery class="content-gallery" >}}
  {{< img src="/images/one.jpg" >}}
  {{< img src="/images/two.jpg" >}}
{{< /gallery >}}
{{< img src="/images/three.jpg" >}}

输出的 HTML 如下。前两个 img 继承了父级 gallery 调用中设置的 class 值 content-gallery,第三个只使用 src:

<div class="content-gallery">
  <img src="/images/one.jpg" class="content-gallery-image">
  <img src="/images/two.jpg" class="content-gallery-image">
</div>
<img src="/images/three.jpg">

渲染顺序

调用短代码所用的写法决定它在 Markdown 渲染的哪个阶段执行:

  1. 用 Markdown 写法调用的短代码在 Markdown 渲染器之前按文档顺序执行。
  2. Markdown 渲染器运行。
  3. 用标准写法调用的短代码在 Markdown 渲染器之后按文档顺序执行。

这意味着,文档中靠前但使用标准写法调用的短代码,仍然晚于靠后但使用 Markdown 写法调用的短代码执行。

在同一阶段内,同一嵌套层级的短代码按文档顺序自上而下执行。短代码嵌套时,Hugo 由内向外渲染:每个嵌套短代码都先于它的父级执行,父级收到的 Inner 内容是所有嵌套短代码渲染完成的输出。例如对于下面的调用:

content/example.md
{{< outer >}}
  {{< inner-a >}}
  {{< inner-b >}}
{{< /outer >}}
{{< standalone >}}

Hugo 的渲染顺序是 inner-a、inner-b、outer(把前两者的渲染结果作为 .Inner 接收),最后是 standalone。

关于模板的返回值

短代码模板不写返回语句,Hugo 把模板渲染出的内容当作该次调用的返回值,直接放在调用处。由于渲染 HTML 时使用的是 Go 的 html/template 包,模板里由数据计算出来的字符串默认会被转义,以免内容破坏页面结构或引入注入风险;模板中直接书写的标签属于静态文本,会原样输出。

因此,当一段由数据拼出的字符串需要作为 HTML 输出时,要显式把它标记为可信内容;只想把它作为纯文本显示时则保持默认的转义即可。编写模板时请把这个区别放在心上:如果转义结果中出现 &lt; 之类的实体,通常说明该内容被当作文本处理了。

其他参考

想找更多思路,可以研究 Hugo 的内置短代码,其源码是很好的范例。

检测短代码是否被使用

HasShortcode 方法可以检查某个短代码是否在页面上被调用过。例如有一个自定义的 audio 短代码:

content/example.md
{{< audio src=/audio/test.mp3 >}}

可以在基础模板中用 HasShortcode 判断该页面是否用过 audio,从而有条件地加载 CSS:

layouts/baseof.html
<head>
  {{ if .HasShortcode "audio" }}
    <link rel="stylesheet" src="/css/audio.css">
  {{ end }}
</head>