跳到主要内容

模板系统

Hugo 使用 Go 模板语言来构建页面。本章将详细介绍模板语法和常用功能。

新模板系统(Hugo v0.146.0+)

Hugo v0.146.0 引入了全新的模板系统,对 layouts 目录结构和模板查找逻辑进行了重大改进。新系统更简洁、更直观,同时保持了向后兼容性。

目录结构变化

变更说明操作
_default 目录被移除layouts/_default 中的所有文件移到 layouts/ 根目录
layouts/partials 重命名为 layouts/_partials重命名文件夹
layouts/shortcodes 重命名为 layouts/_shortcodes重命名文件夹
index.html 模板改名为 home.html重命名文件
移除了 _internal 内部模板概念{{ template "_internal/opengraph.html" . }} 改为 {{ partial "opengraph.html" . }}

新的目录结构示例

layouts
├── baseof.html # 基础模板
├── baseof.term.html # term 页面的基础模板
├── home.html # 首页模板(原 index.html)
├── page.html # 单页模板(原 single.html)
├── section.html # 分区列表模板
├── taxonomy.html # 分类列表模板
├── term.html # 标签页模板
├── _markup # 渲染钩子
│ ├── render-link.html
│ └── render-codeblock-go.html
├── _partials # 局部模板(原 partials)
│ └── header.html
├── _shortcodes # 短代码(原 shortcodes)
│ └── alert.html
├── docs # 特定路径的模板
│ ├── baseof.html # docs 路径的基础模板
│ ├── page.html # docs 路径的单页模板
│ └── _markup
│ └── render-link.html # docs 路径的链接渲染钩子
└── tags # 特定 taxonomy 的模板
├── taxonomy.html
└── term.html

模板命名规则

新系统中,模板文件名可以包含多个标识符,用点号分隔:

<layout>.<kind>.<language>.<output>.<mediatype>

标识符类型(按重要性排序):

标识符说明
Layout customFront Matter 中设置的 layout
Page kindshomesectiontaxonomytermpage
Layouts standard 1listsingle
Output format输出格式:htmlrssjson
Layouts standard 2all(通用布局)
Language语言代码:enzh
Media type媒体类型:htmlxml

命名示例

home.html                    # 首页模板
page.html # 单页模板
section.html # 分区列表模板
list.html # 通用列表模板
all.html # 通用模板(适用于所有页面类型)
term.mylayout.en.rss.xml # 带多个标识符的模板

all 通用模板

新增的 all.html 模板可以匹配所有页面类型。如果只有这一个模板,它将用于所有 HTML 页面渲染:

<!-- layouts/all.html -->
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
{{ end }}

基础模板命名变化

基础模板的命名规则也有变化:

# 旧命名方式
list-baseof.html
single-baseof.html

# 新命名方式
baseof.list.html
baseof.single.html
baseof.page.html
baseof.section.html

路径特定模板

新系统允许在 layouts 目录下创建与页面路径匹配的子目录:

layouts/
├── docs/
│ ├── baseof.html # /docs/ 及其子路径的基础模板
│ ├── page.html # /docs/ 下的单页模板
│ └── api/
│ ├── page.html # /docs/api/ 下的单页模板
│ └── _markup/
│ └── render-link.html # /docs/api/ 的链接渲染钩子
└── tags/
├── taxonomy.html # tags 分类列表模板
├── term.html # tags 标签页模板
└── blue/
└── list.html # tags/blue 的专属模板

迁移指南

如果你有现有的 Hugo 项目,迁移到新模板系统需要:

1. 重命名目录和文件

# 移动 _default 内容到根目录
mv layouts/_default/* layouts/

# 重命名 partials 目录
mv layouts/partials layouts/_partials

# 重命名 shortcodes 目录
mv layouts/shortcodes layouts/_shortcodes

# 重命名首页模板
mv layouts/index.html layouts/home.html 2>/dev/null || true
mv layouts/home.html layouts/home.html 2>/dev/null || true

# 重命名单页模板
mv layouts/single.html layouts/page.html 2>/dev/null || true

# 重命名基础模板
mv layouts/list-baseof.html layouts/baseof.list.html 2>/dev/null || true
mv layouts/single-baseof.html layouts/baseof.page.html 2>/dev/null || true

2. 更新模板引用

<!-- 旧写法 -->
{{ template "_internal/opengraph.html" . }}
{{ template "_internal/twitter_cards.html" . }}

<!-- 新写法 -->
{{ partial "opengraph.html" . }}
{{ partial "twitter_cards.html" . }}

3. 更新 taxonomy 模板

如果之前有一个 taxonomy.html 同时用于 taxonomy 和 term,需要创建两个独立的模板:

<!-- layouts/taxonomy.html -->
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ range .Pages }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}

<!-- layouts/term.html -->
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ range .Pages }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}

向后兼容性

Hugo v0.146.0 会自动将旧的模板结构映射到新结构,大多数现有项目无需立即迁移。但建议在新项目中使用新的目录结构,以获得更好的可维护性。

要检查是否有模板冲突或警告:

hugo --printPathWarnings

模板基础

变量

使用 {{ }} 包裹变量和表达式:

<!-- 输出标题 -->
<h1>{{ .Title }}</h1>

<!-- 输出内容 -->
<div>{{ .Content }}</div>

<!-- 输出站点标题 -->
<p>{{ .Site.Title }}</p>

点号(.)

点号表示当前上下文。在不同位置,点号代表不同的对象:

位置点号代表
单页模板Page 对象
列表模板Page 对象(列表页)
循环中当前迭代项

管道

使用 | 连接多个操作:

<!-- 转换为大写 -->
{{ .Title | upper }}

<!-- 链式操作 -->
{{ .Title | lower | truncate 20 }}

条件判断

if 语句

{{ if .Title }}
<h1>{{ .Title }}</h1>
{{ end }}

{{ if .Params.image }}
<img src="{{ .Params.image }}" alt="{{ .Title }}">
{{ end }}

if-else 语句

{{ if .Description }}
<meta name="description" content="{{ .Description }}">
{{ else }}
<meta name="description" content="{{ .Site.Params.description }}">
{{ end }}

if-else if-else 语句

{{ if eq .Section "posts" }}
<span class="badge">文章</span>
{{ else if eq .Section "tutorials" }}
<span class="badge">教程</span>
{{ else }}
<span class="badge">其他</span>
{{ end }}

条件操作符

操作符说明示例
eq等于{{ if eq .Type "posts" }}
ne不等于{{ if ne .Draft true }}
lt小于{{ if lt .Weight 10 }}
le小于等于{{ if le .Weight 10 }}
gt大于{{ if gt .WordCount 500 }}
ge大于等于{{ if ge .WordCount 500 }}
and{{ if and .Title .Content }}
or{{ if or .Params.featured .Params.pinned }}
not{{ if not .Draft }}

with 语句

with 用于检查变量是否存在并切换上下文:

{{ with .Params.author }}
<p>作者:{{ . }}</p>
{{ else }}
<p>作者:匿名</p>
{{ end }}

{{ with .Params.image }}
<img src="{{ . }}" alt="封面图">
{{ end }}

循环

range 语句

<!-- 遍历页面 -->
{{ range .Site.RegularPages }}
<article>
<h2>{{ .Title }}</h2>
<p>{{ .Summary }}</p>
</article>
{{ end }}

<!-- 遍历标签 -->
{{ range .Params.tags }}
<span class="tag">{{ . }}</span>
{{ end }}

获取索引和值

{{ range $index, $page := .Site.RegularPages }}
<div class="post post-{{ $index }}">
<h2>{{ $page.Title }}</h2>
</div>
{{ end }}

遍历 Map

{{ range $key, $value := .Site.Data.authors }}
<div class="author">
<h3>{{ $key }}</h3>
<p>{{ $value.name }}</p>
</div>
{{ end }}

循环控制

<!-- 只取前 5 个 -->
{{ range first 5 .Site.RegularPages }}
<h2>{{ .Title }}</h2>
{{ end }}

<!-- 反向遍历 -->
{{ range .Site.RegularPages.Reverse }}
<h2>{{ .Title }}</h2>
{{ end }}

<!-- 组合使用 -->
{{ range first 10 (.Site.RegularPages.ByDate.Reverse) }}
<h2>{{ .Title }}</h2>
{{ end }}

循环中断与跳过

Hugo 0.124+ 支持在 range 循环中使用 breakcontinue 语句:

break - 跳出循环

完全终止循环,不再处理后续元素:

{{ range .Pages }}
{{ if .Draft }}
{{ break }} <!-- 遇到草稿就停止整个循环 -->
{{ end }}
<h2>{{ .Title }}</h2>
{{ end }}

continue - 跳过当前迭代

跳过当前元素,继续处理下一个元素:

{{ range .Pages }}
{{ if .Draft }}
{{ continue }} <!-- 跳过草稿,继续处理下一篇 -->
{{ end }}
<h2>{{ .Title }}</h2>
{{ end }}

实际应用场景

<!-- 只显示前 10 篇非草稿文章 -->
{{ $count := 0 }}
{{ range .Pages }}
{{ if .Draft }}
{{ continue }}
{{ end }}

{{ if ge $count 10 }}
{{ break }}
{{ end }}

<h2>{{ .Title }}</h2>
{{ $count = add $count 1 }}
{{ end }}

配合条件使用

<!-- 找到第一篇字数超过 5000 的文章 -->
{{ range .Pages }}
{{ if gt .WordCount 5000 }}
<h2>推荐长文:{{ .Title }}</h2>
<p>字数:{{ .WordCount }}</p>
{{ break }} <!-- 只显示第一篇 -->
{{ end }}
{{ end }}

模板函数参考

Hugo 提供了丰富的内置函数,按功能分类如下。

集合函数(collections)

用于操作数组、切片、Map 和页面集合。

<!-- after: 获取第 N 个元素之后的所有元素 -->
{{ range after 3 .Pages }}
<p>{{ .Title }}</p>
{{ end }}

<!-- first: 获取前 N 个元素 -->
{{ range first 5 .Site.RegularPages }}
<h2>{{ .Title }}</h2>
{{ end }}

<!-- last: 获取最后 N 个元素 -->
{{ range last 3 .Pages }}
<h2>{{ .Title }}</h2>
{{ end }}

<!-- slice: 创建切片 -->
{{ $tags := slice "Hugo" "Web" "Static" }}

<!-- append: 追加元素到切片 -->
{{ $list := slice "a" "b" }}
{{ $list = $list | append "c" "d" }}

<!-- delimit: 用分隔符连接集合元素 -->
{{ delimit .Params.tags ", " }}
{{ delimit .Params.tags ", " " and " }} <!-- 输出: tag1, tag2 and tag3 -->

<!-- dict: 创建 Map -->
{{ $author := dict "name" "张三" "email" "[email protected]" }}

<!-- in: 判断元素是否在集合中 -->
{{ if in .Params.tags "Hugo" }}
<span>Hugo 相关</span>
{{ end }}

<!-- intersect: 获取两个集合的交集 -->
{{ if intersect .Params.tags (slice "Hugo" "Web") }}
<span>相关标签</span>
{{ end }}

<!-- union: 获取两个集合的并集 -->
{{ $all := union .Params.tags .Params.categories }}

<!-- where: 过滤集合 -->
{{ range where .Site.RegularPages "Section" "posts" }}
<h2>{{ .Title }}</h2>
{{ end }}

<!-- where 支持多种比较操作符 -->
{{ range where .Pages "Params.featured" true }}
{{ range where .Pages "Date" "gt" (now.AddDate -1 0 0) }}
{{ range where .Pages "Params.price" "ge" 100 }}

<!-- sort: 排序 -->
{{ range sort .Site.RegularPages "Title" "asc" }}
{{ range sort .Pages "Date" "desc" }}

<!-- reverse: 反转集合 -->
{{ range .Site.RegularPages.Reverse }}

<!-- shuffle: 随机打乱 -->
{{ range shuffle .Pages }}

<!-- uniq: 去重 -->
{{ $unique := .Params.tags | uniq }}

<!-- group: 分组 -->
{{ range .Site.RegularPages | group "Section" }}
<h2>{{ .Key }}</h2>
{{ range .Pages }}
<p>{{ .Title }}</p>
{{ end }}
{{ end }}

比较函数(compare)

<!-- eq: 等于 -->
{{ if eq .Type "posts" }}
<!-- 内容 -->
{{ end }}

<!-- ne: 不等于 -->
{{ if ne .Draft true }}
<!-- 已发布 -->
{{ end }}

<!-- gt: 大于 / lt: 小于 -->
{{ if gt .WordCount 500 }}
<p>长篇文章</p>
{{ end }}

<!-- ge: 大于等于 / le: 小于等于 -->
{{ if le .Params.rating 3 }}
<p>评分较低</p>
{{ end }}

<!-- and / or: 逻辑运算 -->
{{ if and .Title .Content }}
<p>标题和内容都存在</p>
{{ end }}

{{ if or .Params.featured .Params.pinned }}
<span>精选</span>
{{ end }}

<!-- not: 非运算 -->
{{ if not .Draft }}
<span>已发布</span>
{{ end }}

<!-- cond: 三元条件 -->
{{ $class := cond .IsHome "homepage" "inner-page" }}
<div class="{{ $class }}">

<!-- default: 默认值 -->
{{ $desc := .Description | default .Summary }}
{{ $author := .Params.author | default "匿名" }}

字符串函数(strings)

<!-- 大小写转换 -->
{{ .Title | upper }} <!-- 全大写 -->
{{ .Title | lower }} <!-- 全小写 -->
{{ .Title | title }} <!-- 标题格式 -->
{{ "hello world" | strings.FirstUpper }} <!-- Hello world -->

<!-- 截断 -->
{{ .Summary | truncate 100 }}
{{ .Summary | truncate 100 "..." }}

<!-- 替换 -->
{{ .Title | replace "old" "new" }}
{{ "hello world" | replaceRE "w\\w+" "Hugo" }}

<!-- 分割 -->
{{ range split .Params.tags "," }}
<span>{{ . }}</span>
{{ end }}

<!-- 去除空白和字符 -->
{{ .Title | trim " " }}
{{ .Title | strings.TrimSpace }}
{{ .Title | strings.TrimLeft " " }}
{{ .Title | strings.TrimRight "." }}

<!-- 前缀和后缀判断 -->
{{ if hasPrefix .URL "/posts/" }}
<span>文章</span>
{{ end }}

{{ if hasSuffix .File.Ext ".md" }}
<span>Markdown</span>
{{ end }}

<!-- 包含判断 -->
{{ if strings.Contains .Content "Hugo" }}
<span>提及 Hugo</span>
{{ end }}

<!-- 正则匹配 -->
{{ $matches := findRE "<h2>(.*?)</h2>" .Content }}
{{ $submatches := findRESubmatch "<a href=\"(.*?)\">(.*?)</a>" .Content }}

<!-- 重复 -->
{{ "-" | repeat 10 }} <!-- ---------- -->

<!-- 子字符串 -->
{{ "hello world" | slicestr 0 5 }} <!-- hello -->
{{ "hello world" | substr 6 5 }} <!-- world -->

<!-- 字符统计 -->
{{ len .Title }} <!-- 字符串长度 -->
{{ strings.Count "hello" "l" }} <!-- 统计 l 出现次数 -->
{{ strings.RuneCount "你好世界" }} <!-- Unicode 字符数 -->
{{ strings.CountWords .Content }} <!-- 单词数 -->
{{ strings.CountRunes .Content }} <!-- 字符数(不含空白) -->

<!-- 其他 -->
{{ "hello\n\n" | strings.Chomp }} <!-- 去除末尾换行 -->
{{ .Content | truncate 200 }} <!-- 截断为 200 字符 -->

数学函数(math)

<!-- 基本运算 -->
{{ add 1 2 }} <!-- 加法: 3 -->
{{ sub 10 3 }} <!-- 减法: 7 -->
{{ mul 4 5 }} <!-- 乘法: 20 -->
{{ div 20 4 }} <!-- 除法: 5 -->
{{ mod 10 3 }} <!-- 取模: 1 -->

<!-- 多个数值运算 -->
{{ add 1 2 3 4 }} <!-- 10 -->
{{ mul 2 3 4 }} <!-- 24 -->

<!-- 求和与求积 -->
{{ math.Sum 1 2 3 4 5 }} <!-- 15 -->
{{ math.Product 1 2 3 4 }} <!-- 24 -->
{{ math.Sum (slice 1 2 3) }} <!-- 对切片求和 -->

<!-- 最大值和最小值 -->
{{ math.Max 1 5 3 2 }} <!-- 5 -->
{{ math.Min 5 3 8 1 }} <!-- 1 -->
{{ math.Max (slice 1 5 3) }} <!-- 对切片求最大值 -->

<!-- 绝对值 -->
{{ math.Abs -10 }} <!-- 10 -->

<!-- 取整 -->
{{ math.Ceil 3.2 }} <!-- 4 向上取整 -->
{{ math.Floor 3.8 }} <!-- 3 向下取整 -->
{{ math.Round 3.5 }} <!-- 4 四舍五入 -->

<!-- 幂运算 -->
{{ math.Pow 2 10 }} <!-- 1024 -->
{{ math.Sqrt 16 }} <!-- 4 -->

<!-- 对数 -->
{{ math.Log 10 }} <!-- 自然对数 -->

<!-- 三角函数 -->
{{ math.Sin 1.5708 }} <!-- 正弦 -->
{{ math.Cos 0 }} <!-- 余弦 -->
{{ math.Tan 0 }} <!-- 正切 -->
{{ math.Pi }} <!-- 圆周率 -->

<!-- 弧度角度转换 -->
{{ math.ToDegrees 3.14159 }} <!-- 弧度转角度 -->
{{ math.ToRadians 180 }} <!-- 角度转弧度 -->

<!-- 随机数 -->
{{ math.Rand }} <!-- 0.0 到 1.0 之间的随机数 -->

<!-- 计数器 -->
{{ $counter := math.Counter }}
{{ $counter.Set 0 }}
{{ $counter.Add 1 }}
{{ $counter.Value }} <!-- 1 -->

时间函数(time)

Hugo 提供了完整的时间处理功能,包括时间解析、格式化、计算和比较。

获取当前时间

<!-- 获取当前时间 -->
{{ now }}
{{ time.Now }}

解析时间字符串

Hugo 支持多种时间格式的解析:

<!-- 使用 time.AsTime 解析时间字符串 -->
{{ $t := time.AsTime "2024-01-15" }}
{{ $t := "2024-01-15T10:30:00" | time.AsTime }}
{{ $t := "2024/01/15 10:30:00" | time.AsTime }}

time.AsTime 函数能够智能识别常见的时间格式,包括 ISO 8601、RFC 3339 等标准格式。

格式化日期

Go 语言的日期格式化采用了一种独特的方式:使用特定的参考时间 2006-01-02 15:04:05 来定义格式。这个参考时间的各个部分代表了不同的时间单位:

格式串含义示例
2006四位年份2024
06两位年份24
01两位月份01
Jan月份缩写Jan
January月份全称January
02两位日期15
Monday星期全称Monday
Mon星期缩写Mon
1524小时制小时14
0312小时制小时02
04分钟30
0545
.000毫秒.123
-07:00时区+08:00
<!-- 常用格式化示例 -->
{{ .Date.Format "2006-01-02" }} <!-- 2024-01-15 -->
{{ .Date.Format "2006年01月02日" }} <!-- 2024年01月15日 -->
{{ .Date.Format "Jan 02, 2006" }} <!-- Jan 15, 2024 -->
{{ .Date.Format "2006-01-02 15:04:05" }} <!-- 2024-01-15 14:30:45 -->
{{ .Date.Format "Mon, Jan 2, 2006" }} <!-- Mon, Jan 15, 2024 -->
{{ .Date.Format "2006-01-02T15:04:05Z07:00" }} <!-- ISO 8601 格式 -->

<!-- 显示星期 -->
{{ .Date.Format "2006年01月02日 Monday" }} <!-- 2024年01月15日 Monday -->

预定义格式

Hugo 提供了一些预定义的本地化格式,这些格式会根据站点的语言设置自动调整:

{{ .Date | time.Format ":date_long" }}    <!-- 2024年1月15日 -->
{{ .Date | time.Format ":date_short" }} <!-- 2024/1/15 -->
{{ .Date | time.Format ":time_long" }} <!-- 14:30:45 -->
{{ .Date | time.Format ":time_short" }} <!-- 14:30 -->
{{ .Date | time.Format ":date_medium" }} <!-- 2024年1月15日 -->
{{ .Date | time.Format ":date_full" }} <!-- 2024年1月15日星期一 -->

时间计算

<!-- 解析持续时间 -->
{{ $duration := time.ParseDuration "2h30m" }} <!-- 2小时30分钟 -->
{{ $duration := time.ParseDuration "1h45m30s" }} <!-- 1小时45分30秒 -->

<!-- 时间加减 -->
{{ $future := now.Add $duration }} <!-- 加上持续时间 -->
{{ $past := now.AddDate -1 0 0 }} <!-- 一年前 -->
{{ $nextMonth := now.AddDate 0 1 0 }} <!-- 一个月后 -->
{{ $nextYear := now.AddDate 1 0 0 }} <!-- 一年后 -->

<!-- AddDate 参数:年、月、日 -->
{{ $date := now.AddDate 0 0 7 }} <!-- 7天后 -->

时间比较

<!-- 比较时间先后 -->
{{ if gt .Date (now.AddDate -1 0 0) }}
<span>这是今年发布的文章</span>
{{ end }}

{{ if lt .Date (now.AddDate -1 0 0) }}
<span>这是一年前的文章</span>
{{ end }}

<!-- 判断是否为未来日期 -->
{{ if gt .Date now }}
<span>定时发布</span>
{{ end }}

时间差计算

<!-- 计算时间差 -->
{{ $diff := now.Sub .Date }} <!-- 返回 Duration -->
{{ $hours := $diff.Hours }} <!-- 相差小时数 -->
{{ $days := $diff.Hours | div 24 }} <!-- 相差天数 -->

<!-- 显示相对时间 -->
{{ $days := now.Sub .Date | int | div 86400 }}
{{ if lt $days 7 }}
<span>{{ $days }} 天前发布</span>
{{ else if lt $days 30 }}
<span>{{ div $days 7 }} 周前发布</span>
{{ else }}
<span>{{ .Date.Format "2006-01-02" }}</span>
{{ end }}

实际应用示例

显示文章的发布时间信息:

<time datetime="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
{{ $days := now.Sub .Date | int | div 86400 }}
{{ if lt $days 1 }}
今天
{{ else if lt $days 2 }}
昨天
{{ else if lt $days 7 }}
{{ $days }} 天前
{{ else }}
{{ .Date.Format "2006年01月02日" }}
{{ end }}
</time>

创建文章归档按年月分组:

{{ range .Site.RegularPages.GroupByDate "2006-01" }}
<h2>{{ .Key | time.AsTime | time.Format "2006年01月" }}</h2>
<ul>
{{ range .Pages }}
<li>{{ .Title }}</li>
{{ end }}
</ul>
{{ end }}

URL 函数(urls)

<!-- 相对 URL -->
{{ .RelPermalink }}

<!-- 绝对 URL -->
{{ .Permalink }}

<!-- 带语言前缀的 URL -->
{{ urls.AbsLangURL "/about/" }}
{{ urls.RelLangURL "/about/" }}

<!-- URL 编码 -->
{{ "hello world" | urlquery }}
{{ .Title | urls.URLize }} <!-- 转为 URL 友好格式 -->

<!-- URL 解析 -->
{{ $url := urls.Parse "https://example.com/path?q=1" }}
{{ $url.Scheme }} <!-- https -->
{{ $url.Host }} <!-- example.com -->
{{ $url.Path }} <!-- /path -->

<!-- 创建锚点 ID -->
{{ "标题文字" | urls.Anchorize }}

<!-- 页面引用 -->
{{ ref . "about.md" }}
{{ relref . "posts/my-post.md" }}

类型转换函数(cast)

<!-- 转换为字符串 -->
{{ $str := cast.ToString 123 }}

<!-- 转换为整数 -->
{{ $int := cast.ToInt "42" }}

<!-- 转换为浮点数 -->
{{ $float := cast.ToFloat "3.14" -->

编码函数(encoding)

<!-- Base64 编码和解码 -->
{{ $encoded := "hello" | encoding.Base64Encode }}
{{ $decoded := $encoded | encoding.Base64Decode }}

<!-- JSON 编码 -->
{{ $json := dict "name" "Hugo" | encoding.Jsonify }}
{{ $json := slice 1 2 3 | jsonify }}

加密函数(crypto)

<!-- MD5 哈希 -->
{{ crypto.MD5 "hello" }}

<!-- SHA 哈希 -->
{{ crypto.SHA1 "hello" }}
{{ crypto.SHA256 "hello" }}

<!-- HMAC -->
{{ crypto.HMAC "sha256" "secret-key" "message" }}

路径函数(path)

<!-- 路径各部分 -->
{{ path.Base "/a/b/c.txt" }} <!-- c.txt -->
{{ path.BaseName "/a/b/c.txt" }} <!-- c -->
{{ path.Dir "/a/b/c.txt" }} <!-- /a/b -->
{{ path.Ext "/a/b/c.txt" }} <!-- .txt -->

<!-- 路径连接和清理 -->
{{ path.Join "a" "b" "c" }} <!-- a/b/c -->
{{ path.Clean "a/./b/../c" }} <!-- a/c -->

<!-- 路径分割 -->
{{ $parts := path.Split "/a/b/c" }}

转换函数(transform)

<!-- Markdown 转 HTML -->
{{ .Summary | transform.Markdownify }}

<!-- HTML 转纯文本 -->
{{ .Content | transform.Plainify }}

<!-- HTML 转义 -->
{{ $content | transform.HTMLEscape }}
{{ $content | transform.HTMLUnescape }}

<!-- 代码高亮 -->
{{ transform.Highlight .Code "go" }}
{{ transform.Highlight .Code "go" "linenos=table" }}

<!-- Emoji 处理 -->
{{ ":smile:" | transform.Emojify }}

<!-- 解析数据 -->
{{ $data := $json | transform.Unmarshal }} <!-- JSON/CSV/YAML/TOML/XML -->

<!-- 数学公式 -->
{{ $math := transform.ToMath "E = mc^2" }}

语言函数(lang)

用于多语言网站的本地化处理,包括翻译、数字格式化、货币格式化等功能。

翻译函数

使用翻译键获取当前语言的翻译字符串:

<!-- 基本翻译 -->
{{ i18n "readMore" }}
{{ T "postedOn" }}
{{ lang.Translate "welcome" }}

<!-- 带变量的翻译 -->
{{ i18n "readingTime" .ReadingTime }}

<!-- 带复数的翻译 -->
{{ i18n "postCount" (len .Pages) }}

翻译文件示例(i18n/zh-cn.toml):

[readMore]
other = "阅读更多"

[postedOn]
other = "发布于"

[readingTime]
one = "一分钟阅读"
other = "{{ .Count }} 分钟阅读"

[postCount]
one = "一篇文章"
other = "{{ .Count }} 篇文章"

数字格式化

根据当前语言环境的规则格式化数字:

基本数字格式化

{{ lang.FormatNumber 1234567.89 }}
<!-- 英文输出: 1,234,567.89 -->
<!-- 德文输出: 1.234.567,89 -->
<!-- 法文输出: 1 234 567,89 -->

自定义数字格式化

<!-- 指定小数位数 -->
{{ lang.FormatNumberCustom 2 "-,." . }}
<!-- 输出: 1-234-567.89 -->

<!-- 不同格式模式 -->
{{ lang.FormatNumberCustom 2 "0.000" . }} <!-- 1234567.890 -->
{{ lang.FormatNumberCustom 2 "#,###" . }} <!-- 1,234,568 -->
{{ lang.FormatNumberCustom 2 "#,##0.00" . }} <!-- 1,234,567.89 -->

格式模式字符

字符说明
0数字占位符,不足位数补零
#数字占位符,不足位数不显示
,分组分隔符
.小数分隔符
-分组分隔符(自定义)

货币格式化

根据货币代码和语言环境格式化货币值:

基本货币格式化

{{ lang.FormatCurrency 2 "USD" 1234.56 }}
<!-- 英文输出: $1,234.56 -->
<!-- 德文输出: 1.234,56 $ -->

{{ lang.FormatCurrency 2 "EUR" 1234.56 }}
<!-- 英文输出: €1,234.56 -->
<!-- 德文输出: 1.234,56 € -->

{{ lang.FormatCurrency 2 "CNY" 1234.56 }}
<!-- 中文输出: ¥1,234.56 -->
<!-- 英文输出: CN¥1,234.56 -->

会计格式

会计格式将负数放在括号中:

{{ lang.FormatAccounting 2 "USD" 1234.56 }}   <!-- $1,234.56 -->
{{ lang.FormatAccounting 2 "USD" -1234.56 }} <!-- ($1,234.56) -->

常用货币代码

代码货币
USD美元
EUR欧元
GBP英镑
CNY人民币
JPY日元
CAD加拿大元
AUD澳大利亚元

百分比格式化

将小数格式化为百分比:

{{ lang.FormatPercent 2 0.156 }}     <!-- 15.60% -->
{{ lang.FormatPercent 0 0.875 }} <!-- 88% -->
{{ lang.FormatPercent 1 1.0 }} <!-- 100.0% -->

实际应用示例

价格显示

{{ $price := 99.99 }}
{{ $currency := .Site.Params.currency | default "USD" }}
<p>价格: {{ lang.FormatCurrency 2 $currency $price }}</p>

统计数据展示

{{ $views := 1234567 }}
{{ $growth := 0.156 }}

<div class="stats">
<p>访问量: {{ lang.FormatNumber 0 $views }}</p>
<p>增长率: {{ lang.FormatPercent 1 $growth }}</p>
</div>

多语言价格表

{{ $products := slice
(dict "name" "基础版" "price" 9.99)
(dict "name" "专业版" "price" 29.99)
(dict "name" "企业版" "price" 99.99)
}}

{{ $currency := cond (eq .Lang "zh") "CNY" "USD" }}

<table class="price-table">
{{ range $products }}
<tr>
<td>{{ .name }}</td>
<td>{{ lang.FormatCurrency 2 $currency .price }}</td>
</tr>
{{ end }}
</table>

合并翻译

将默认语言的翻译合并到当前语言:

{{ $translations := lang.Merge .Site.Data.i18n.en .Site.Data.i18n.zh }}

这在需要确保翻译完整性的场景中很有用。

安全函数(safe)

用于标记内容为安全,避免模板自动转义。

<!-- 标记为安全 HTML -->
{{ $html | safeHTML }}

<!-- 标记为安全 CSS -->
{{ $css | safeCSS }}

<!-- 标记为安全 JavaScript -->
{{ $js | safeJS }}

<!-- 标记为安全 URL -->
{{ $url | safeURL }}

<!-- 标记为安全 HTML 属性 -->
{{ $attr | safeHTMLAttr }}

调试函数(debug)

<!-- 打印变量内容 -->
{{ debug.Dump .Params }}

<!-- 计时器 -->
{{ debug.Timer "my-operation" }}

哈希函数(hash)

非加密哈希函数,用于生成快速哈希值。

<!-- FNV32a 哈希 -->
{{ hash.FNV32a "hello" }}

<!-- xxHash 哈希(更快) -->
{{ hash.XxHash "hello" }}

与 crypto 函数不同,hash 函数不是为安全目的设计的,而是用于快速生成标识符或缓存键。

单词变形函数(inflect)

用于英语单词的变形处理。

<!-- 单数化 -->
{{ "cats" | singularize }} <!-- cat -->

<!-- 复数化 -->
{{ "cat" | pluralize }} <!-- cats -->

<!-- 人性化 -->
{{ "camelCase" | humanize }} <!-- Camel case -->

这些函数对于生成友好的文本标签很有用。

类型反射函数(reflect)

用于判断值的数据类型。

<!-- 判断是否为 Map -->
{{ if reflect.IsMap .Params }}
<!-- 是 Map 类型 -->
{{ end }}

<!-- 判断是否为 Slice -->
{{ if reflect.IsSlice .Params.tags }}
<!-- 是切片类型 -->
{{ end }}

在需要根据数据类型进行不同处理时很有用。

格式化函数(fmt)

用于模板中的输出和日志记录。

<!-- 格式化字符串 -->
{{ printf "Hello, %s!" "World" }}

<!-- 打印到控制台 -->
{{ print "Debug: " .Title }}
{{ println "Line with newline" }}

<!-- 错误输出(会中断构建) -->
{{ errorf "找不到页面: %s" .Path }}

<!-- 可抑制的错误 -->
{{ erroridf "my-error-id" "错误信息: %s" .Value }}

<!-- 警告输出 -->
{{ warnf "配置项 %s 已弃用" "oldOption" }}

<!-- 可抑制的警告 -->
{{ warnidf "my-warn-id" "警告信息: %s" .Value }}

errorfwarnf 对于调试和验证很有用。使用 erroridfwarnidf 可以通过配置抑制特定警告:

ignoreLogs = ['my-warn-id']

模板函数(templates)

用于查询和操作模板系统。

<!-- 检查模板是否存在 -->
{{ if templates.Exists "partials/header.html" }}
<!-- 存在 -->
{{ end }}

<!-- 延迟执行模板(在所有站点和输出格式渲染后执行) -->
{{ templates.Defer }}

templates.Defer 是一个高级功能,用于在构建的最后阶段执行特定模板代码,适用于需要访问完整构建结果的场景。

图表函数(diagrams)

用于渲染图表。

<!-- GoAT 图表(ASCII 艺术转 SVG) -->
{{ $diagram := diagrams.Goat " +---+ +---+
| A | --> | B |
+---+ +---+" }}

GoAT 是 Hugo 内置支持的 ASCII 图表渲染器,可以将简单的 ASCII 图表转换为 SVG 格式。

OpenAPI 函数(openapi3)

用于处理 OpenAPI 3 规范文件。

<!-- 解析 OpenAPI 规范 -->
{{ $spec := resources.Get "api/openapi.yaml" | openapi3.Unmarshal }}

<!-- 访问 API 信息 -->
<h1>{{ $spec.Info.Title }}</h1>
<p>{{ $spec.Info.Version }}</p>

这对于从 OpenAPI 规范生成 API 文档非常有用。

操作系统函数(os)

<!-- 读取环境变量 -->
{{ os.Getenv "HUGO_ENV" }}

<!-- 检查文件是否存在 -->
{{ if os.FileExists "/path/to/file" }}
<!-- 存在 -->
{{ end }}

<!-- 读取文件内容 -->
{{ $content := os.ReadFile "README.md" }}

<!-- 读取目录 -->
{{ $files := os.ReadDir "content" }}

<!-- 获取文件信息 -->
{{ $info := os.Stat "content/_index.md" }}

Hugo 函数(hugo)

获取 Hugo 环境信息。

<!-- 版本信息 -->
{{ hugo.Version }} <!-- 0.121.1 -->
{{ hugo.BuildDate }}
{{ hugo.CommitHash }}
{{ hugo.GoVersion }}

<!-- 环境判断 -->
{{ hugo.Environment }} <!-- production 或 development -->
{{ if hugo.IsProduction }} <!-- 是否生产环境 -->
{{ if hugo.IsDevelopment }} <!-- 是否开发环境 -->
{{ if hugo.IsServer }} <!-- 是否运行 hugo server -->

<!-- 功能检测 -->
{{ if hugo.IsExtended }} <!-- 是否 Extended 版本 -->
{{ if hugo.IsMultilingual }} <!-- 是否多语言站点 -->
{{ if hugo.IsMultihost }} <!-- 是否多主机配置 -->

<!-- 其他 -->
{{ hugo.Generator }} <!-- 生成 meta 标签 -->
{{ hugo.WorkingDir }} <!-- 工作目录 -->
{{ hugo.Deps }} <!-- 项目依赖列表 -->

<!-- 全局存储 -->
{{ hugo.Store.Set "key" "value" }}
{{ hugo.Store.Get "key" }}

模板函数(templates)

<!-- 检查模板是否存在 -->
{{ if templates.Exists "partials/header.html" }}
<!-- 存在 -->
{{ end }}

<!-- 延迟执行模板 -->
{{ templates.Defer }}

Partials(局部模板)

Partials 是可复用的模板片段,存放在 layouts/partials/ 目录。

创建 Partial

layouts/partials/header.html

<header class="site-header">
<div class="logo">
<a href="{{ .Site.BaseURL }}">{{ .Site.Title }}</a>
</div>
<nav>
{{ range .Site.Menus.main }}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</nav>
</header>

使用 Partial

<!-- 传递当前上下文 -->
{{ partial "header.html" . }}

<!-- 不传递上下文 -->
{{ partial "header.html" .Site }}

Partial 返回值

Partial 可以返回值而不是直接输出:

layouts/partials/reading-time.html

{{ $words := .WordCount }}
{{ $minutes := div $words 200 }}
{{ return $minutes }}

使用:

{{ $time := partial "reading-time.html" . }}
<p>预计阅读时间:{{ $time }} 分钟</p>

Partial Decorators(部分装饰器)

Partial Decorators 是一种特殊的 partial,它作为包装器组件使用。与标准 partial 不同,装饰器使用组合方式包裹整个内容块,通过 templates.Inner 函数定义外部内容应该注入的位置。

基本概念

传统的 partial 只是在固定模板中渲染数据,而装饰器可以包裹任意内容块。这种模式将容器逻辑与内容逻辑分离,提高了模板的可复用性。

基本用法

调用装饰器

使用 with 语句和块式调用:

{{ with partial "components/wrapper.html" . }}
<p>这个块中的所有内容都会被包装。</p>
<p>{{ .Content | transform.Plainify | strings.Truncate 200 }}</p>
{{ end }}

装饰器模板 (layouts/partials/components/wrapper.html):

<div class="wrapper-styling">
{{ templates.Inner . }}
</div>

templates.Inner 函数定义了被包裹内容出现的位置。

上下文传递

with 语句创建新的作用域,外部变量在块内不可用。可以通过传递上下文解决:

{{ $ctx := dict
"page" .
"title" "文章列表"
}}
{{ with partial "components/section.html" $ctx }}
<h2>{{ .page.Title }}</h2>
{{ range .page.Pages }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}

templates.Inner 函数可以接受上下文参数,定义块内点号 . 代表的内容:

<!-- 装饰器模板 -->
<div class="card">
{{ with .title }}
<h2>{{ . }}</h2>
{{ end }}
<div class="card-body">
{{ templates.Inner $.page }}
</div>
</div>

嵌套装饰器

装饰器支持多层嵌套,每层都可以传递不同的上下文:

<!-- 首页模板 -->
{{ $ctx := dict
"page" .
"label" "最新文章"
"pageCollection" ((site.GetPage "/posts").RegularPages)
}}

{{ with partial "components/section.html" $ctx }}
<div class="grid-wrapper">
{{ range .pageCollection }}
{{ with partial "components/column.html" (dict "page" . "class" "col-half") }}
{{ with partial "components/card.html" (dict "page" .page "url" .page.RelPermalink "title" .page.LinkTitle) }}
<p>{{ .page.Content | plainify | strings.Truncate 240 }}</p>
{{ end }}
{{ end }}
{{ end }}
</div>
{{ end }}

Section 组件 (layouts/partials/components/section.html):

<section class="content-section">
{{ with .label }}
<h2 class="section-label">{{ . }}</h2>
{{ end }}
<div class="section-content">
{{ templates.Inner . }}
</div>
</section>

Column 组件 (layouts/partials/components/column.html):

<div class="{{ .class | default `column-default` }}">
{{ templates.Inner . }}
</div>

Card 组件 (layouts/partials/components/card.html):

<div class="card">
{{ with .title }}
<h2 class="card-title">
{{ if $.url }}
<a href="{{ $.url }}">{{ . }}</a>
{{ else }}
{{ . }}
{{ end }}
</h2>
{{ end }}
<div class="card-body">
{{ templates.Inner . }}
</div>
{{ with .url }}
<div class="card-footer">
<a href="{{ . }}">阅读更多</a>
</div>
{{ end }}
</div>

装饰器的优势

使用 Partial Decorators 有以下好处:

消除分离的开启/关闭 partial

传统方式需要两个 partial:

<!-- 传统方式 -->
{{ partial "components/wrapper-start.html" . }}
<p>内容</p>
{{ partial "components/wrapper-end.html" . }}

装饰器方式更简洁:

{{ with partial "components/wrapper.html" . }}
<p>内容</p>
{{ end }}

避免参数膨胀

标准 partial 需要为所有可能的内容变化准备参数:

<!-- 参数过多 -->
{{ partial "card.html" (dict
"title" "标题"
"content" "内容"
"footer" "底部"
"image" "图片"
"class" "样式类"
...
) }}

装饰器让调用者控制内部内容:

{{ with partial "card.html" (dict "title" "标题") }}
<!-- 调用者完全控制内容 -->
<img src="custom-image.jpg" alt="">
<p>任意内容</p>
<a href="/link">自定义链接</a>
{{ end }}

清晰的组合模式

容器处理结构,调用者控制内容:

<!-- 装饰器处理容器样式 -->
{{ with partial "components/grid.html" (dict "columns" 3) }}
<!-- 调用者控制每个单元格 -->
{{ range .items }}
<div class="item">{{ . }}</div>
{{ end }}
{{ end }}

实际应用场景

布局容器

{{ with partial "layouts/two-column.html" (dict "page" .) }}
<main>
{{ templates.Inner .page }}
</main>
<aside>
{{ partial "sidebar.html" .page }}
</aside>
{{ end }}

表单包装器

{{ with partial "components/form-wrapper.html" (dict "action" "/submit" "method" "POST") }}
<input type="text" name="username">
<input type="email" name="email">
<button type="submit">提交</button>
{{ end }}

模态框

{{ with partial "components/modal.html" (dict "id" "login-modal" "title" "登录") }}
<form>
<input type="text" name="username">
<input type="password" name="password">
</form>
{{ end }}

与标准 Partial 的对比

特性标准 PartialPartial Decorator
内容来源模板内部定义调用者提供
灵活性参数控制任意模板代码
作用域单一上下文可传递上下文
使用场景固定内容结构可变内容容器

选择建议:

  • 内容结构固定,只需参数化 → 使用标准 partial
  • 内容结构多变,需要包装器 → 使用 partial decorator

Base Template(基础模板)

Base Template 定义页面的基础结构,其他模板继承它。

定义 Base Template

layouts/_default/baseof.html

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
<meta charset="UTF-8">
<title>{{ .Title }} | {{ .Site.Title }}</title>
{{ block "head" . }}{{ end }}
</head>
<body>
{{ partial "header.html" . }}

<main>
{{ block "main" . }}{{ end }}
</main>

{{ partial "footer.html" . }}

{{ block "scripts" . }}{{ end }}
</body>
</html>

继承 Base Template

layouts/_default/single.html

{{ define "head" }}
<meta name="description" content="{{ .Description }}">
{{ end }}

{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
<time>{{ .Date.Format "2006-01-02" }}</time>
{{ .Content }}
</article>
{{ end }}

{{ define "scripts" }}
<script src="/js/post.js"></script>
{{ end }}

列表模板

列表模板用于显示内容列表。

基本列表模板

layouts/_default/list.html

{{ define "main" }}
<h1>{{ .Title }}</h1>

{{ .Content }}

<div class="posts">
{{ range .Paginator.Pages }}
<article>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<time>{{ .Date.Format "2006-01-02" }}</time>
<p>{{ .Summary }}</p>
</article>
{{ end }}
</div>

{{ template "_internal/pagination.html" . }}
{{ end }}

分页

Hugo 内置分页功能:

<!-- 配置每页数量 -->
[pagination]
pagerSize = 10

<!-- 使用分页 -->
{{ $paginator := .Paginate .Pages }}
{{ range $paginator.Pages }}
<article>
<h2>{{ .Title }}</h2>
</article>
{{ end }}

<!-- 分页导航 -->
{{ template "_internal/pagination.html" . }}

<!-- 自定义分页导航 -->
<nav class="pagination">
{{ if $paginator.HasPrev }}
<a href="{{ $paginator.Prev.URL }}">上一页</a>
{{ end }}

<span>{{ $paginator.PageNumber }} / {{ $paginator.TotalPages }}</span>

{{ if $paginator.HasNext }}
<a href="{{ $paginator.Next.URL }}">下一页</a>
{{ end }}
</nav>

单页模板

单页模板用于显示单个内容页面。

layouts/_default/single.html

{{ define "main" }}
<article class="post">
<header>
<h1>{{ .Title }}</h1>
<div class="meta">
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "2006年01月02日" }}
</time>
{{ with .Params.author }}
<span class="author">{{ . }}</span>
{{ end }}
</div>
{{ with .Params.tags }}
<div class="tags">
{{ range . }}
<a href="{{ "tags/" | relURL }}{{ . | urlize }}/">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</header>

{{ with .Params.image }}
<img src="{{ . }}" alt="{{ $.Title }}" class="cover">
{{ end }}

<div class="content">
{{ .Content }}
</div>

<footer>
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}">上一篇:{{ .Title }}</a>
{{ end }}
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}">下一篇:{{ .Title }}</a>
{{ end }}
</footer>
</article>
{{ end }}

首页模板

首页模板是网站入口页面的模板。

layouts/index.html

{{ define "main" }}
<section class="hero">
<h1>{{ .Site.Title }}</h1>
<p>{{ .Site.Params.description }}</p>
</section>

<section class="featured">
<h2>精选文章</h2>
{{ range first 5 (where .Site.RegularPages "Section" "posts") }}
<article>
<h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
<p>{{ .Summary }}</p>
</article>
{{ end }}
</section>

<section class="recent">
<h2>最新文章</h2>
{{ range first 10 .Site.RegularPages }}
<article>
<h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
<time>{{ .Date.Format "2006-01-02" }}</time>
</article>
{{ end }}
</section>
{{ end }}

Section 模板

Section 模板用于特定内容分区。

layouts/posts/list.html

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}

<div class="posts">
{{ range .Pages }}
<article>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<time>{{ .Date.Format "2006-01-02" }}</time>
<p>{{ .Summary }}</p>
{{ with .Params.tags }}
<div class="tags">
{{ range . }}<span>{{ . }}</span>{{ end }}
</div>
{{ end }}
</article>
{{ end }}
</div>
{{ end }}

Taxonomy 模板

Taxonomy 模板用于分类和标签页面。

layouts/_default/terms.html(所有标签/分类列表):

{{ define "main" }}
<h1>{{ .Title }}</h1>

<ul class="taxonomy-list">
{{ range .Data.Terms.Alphabetical }}
<li>
<a href="{{ .Page.RelPermalink }}">{{ .Page.Title }}</a>
<span class="count">({{ .Count }})</span>
</li>
{{ end }}
</ul>
{{ end }}

layouts/_default/taxonomy.html(特定标签/分类页面):

{{ define "main" }}
<h1>{{ .Title }}</h1>

<div class="posts">
{{ range .Pages }}
<article>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<time>{{ .Date.Format "2006-01-02" }}</time>
</article>
{{ end }}
</div>
{{ end }}

模板查找顺序

Hugo 按照特定顺序查找模板,优先使用更具体的模板。

新模板系统(v0.146.0+)

新系统使用统一的模板查找逻辑,标识符优先级如下:

  1. Layout custom - Front Matter 中设置的 layout
  2. Page kinds - homesectiontaxonomytermpage
  3. Layouts standard 1 - listsingle
  4. Output format - 输出格式(htmlrss 等)
  5. Layouts standard 2 - all(通用布局)
  6. Language - 语言代码
  7. Media type - 媒体类型
  8. Page path - 页面路径
  9. Type - Front Matter 中的 type

查找示例

对于一个位于 /posts/my-post/ 的页面:

  1. layouts/posts/page.html - 路径特定模板
  2. layouts/posts/mylayout.html - 自定义 layout(如果设置了 layout: mylayout
  3. layouts/page.html - 通用单页模板
  4. layouts/all.html - 通用模板
  5. 主题中的对应模板

旧模板系统(v0.145 及之前)

旧的查找顺序:

  1. layouts/<section>/single.html
  2. layouts/<section>/list.html
  3. layouts/_default/single.html
  4. layouts/_default/list.html
  5. themes/<theme>/layouts/...

页面方法参考

Page 对象提供了丰富的方法和属性,用于获取页面信息。

内容相关

方法说明示例
.Content渲染后的内容{{ .Content }}
.Plain纯文本内容(去除 HTML){{ .Plain }}
.PlainWords纯文本单词数组{{ len .PlainWords }}
.RawContent原始 Markdown 内容{{ .RawContent }}
.Summary内容摘要{{ .Summary }}
.Truncated摘要是否被截断{{ if .Truncated }}...{{ end }}
.WordCount字数统计{{ .WordCount }}
.ReadingTime预计阅读时间(分钟){{ .ReadingTime }}
.FuzzyWordCount模糊字数(向上取整到百位){{ .FuzzyWordCount }}
.Len内容字节长度{{ .Len }}

元数据相关

方法说明示例
.Title页面标题{{ .Title }}
.LinkTitle链接标题(用于导航){{ .LinkTitle }}
.Description页面描述{{ .Description }}
.Keywords关键词数组{{ .Keywords }}
.Date发布日期{{ .Date.Format "2006-01-02" }}
.Lastmod最后修改日期{{ .Lastmod }}
.PublishDate发布日期{{ .PublishDate }}
.ExpiryDate过期日期{{ .ExpiryDate }}
.Draft是否草稿{{ .Draft }}
.Weight权重{{ .Weight }}
.AliasesURL 别名{{ range .Aliases }}{{ . }}{{ end }}

URL 和路径

方法说明示例
.Permalink绝对 URL{{ .Permalink }}
.RelPermalink相对 URL{{ .RelPermalink }}
.SlugURL slug{{ .Slug }}
.Path逻辑路径{{ .Path }}
.Section顶级分区名{{ .Section }}
.Type内容类型{{ .Type }}
.Kind页面类型{{ .Kind }}

分类法相关

方法说明示例
.GetTerms获取指定分类法的术语{{ range .GetTerms "tags" }}{{ .Title }}{{ end }}
.Params.tags标签{{ range .Params.tags }}{{ . }}{{ end }}
.Params.categories分类{{ range .Params.categories }}{{ . }}{{ end }}

导航相关

方法说明示例
.Next站点中的下一页{{ with .Next }}{{ .Title }}{{ end }}
.Prev站点中的上一页{{ with .Prev }}{{ .Title }}{{ end }}
.NextInSection分区中的下一页{{ with .NextInSection }}{{ .Title }}{{ end }}
.PrevInSection分区中的上一页{{ with .PrevInSection }}{{ .Title }}{{ end }}
.Pages当前分区的页面集合{{ range .Pages }}{{ end }}
.RegularPages常规页面{{ range .RegularPages }}{{ end }}
.RegularPagesRecursive递归获取常规页面{{ range .RegularPagesRecursive }}{{ end }}

分区和层级

方法说明示例
.CurrentSection当前分区{{ .CurrentSection.Title }}
.FirstSection顶级分区{{ .FirstSection.Title }}
.Parent父分区{{ with .Parent }}{{ .Title }}{{ end }}
.Sections子分区{{ range .Sections }}{{ .Title }}{{ end }}
.Ancestors祖先分区{{ range .Ancestors }}{{ .Title }}{{ end }}
.IsAncestor是否为祖先{{ if .IsAncestor $other }}
.IsDescendant是否为后代{{ if .IsDescendant $other }}
.InSection是否在指定分区{{ if .InSection $section }}

多语言相关

方法说明示例
.Language语言对象{{ .Language.LanguageName }}
.IsTranslated是否有翻译{{ if .IsTranslated }}
.Translations其他语言版本{{ range .Translations }}{{ end }}
.AllTranslations所有语言版本{{ range .AllTranslations }}{{ end }}
.TranslationKey翻译键{{ .TranslationKey }}

页面资源

方法说明示例
.Resources页面资源集合{{ range .Resources }}{{ end }}
.BundleType页面包类型{{ .BundleType }}

其他方法

方法说明示例
.IsHome是否首页{{ if .IsHome }}
.IsPage是否常规页面{{ if .IsPage }}
.IsSection是否分区页面{{ if .IsSection }}
.IsNode是否节点页面{{ if .IsNode }}
.TableOfContents目录{{ .TableOfContents }}
.File文件信息{{ .File.Path }}
.GitInfoGit 信息{{ with .GitInfo }}{{ .AuthorName }}{{ end }}
.Params自定义参数{{ .Params.custom }}
.Param获取参数(回退到站点){{ .Param "description" }}
.Scratch临时存储{{ .Scratch.Set "key" "value" }}
.Store持久存储{{ .Store.Set "key" "value" }}

常用页面方法示例

<!-- 获取阅读时间 -->
<p>预计阅读 {{ .ReadingTime }} 分钟</p>

<!-- 显示字数 -->
<p>共 {{ .WordCount }} 字</p>

<!-- 判断是否首页 -->
{{ if .IsHome }}
<h1>欢迎来到首页</h1>
{{ end }}

<!-- 获取上一篇和下一篇文章 -->
<nav class="post-nav">
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}" class="prev">
← {{ .Title }}
</a>
{{ end }}
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}" class="next">
{{ .Title }} →
</a>
{{ end }}
</nav>

<!-- 获取页面的所有标签 -->
{{ with .GetTerms "tags" }}
<div class="tags">
{{ range . }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
</div>
{{ end }}

<!-- 获取面包屑导航 -->
<nav class="breadcrumb">
{{ range .Ancestors.Reverse }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a> /
{{ end }}
<span>{{ .Title }}</span>
</nav>

<!-- 获取页面资源 -->
{{ with .Resources.GetMatch "cover.*" }}
<img src="{{ .RelPermalink }}" alt="封面">
{{ end }}

<!-- 获取目录 -->
{{ if .Params.toc | default true }}
<aside class="toc">
<h3>目录</h3>
{{ .TableOfContents }}
</aside>
{{ end }}

<!-- 使用 Scratch 存储临时数据 -->
{{ .Scratch.Set "counter" 0 }}
{{ range .Pages }}
{{ $.Scratch.Add "counter" 1 }}
{{ end }}
<p>共 {{ .Scratch.Get "counter" }} 篇文章</p>

Site 对象参考

通过 .Site 可以访问站点级别的信息。

站点属性

属性说明示例
.Site.Title站点标题{{ .Site.Title }}
.Site.BaseURL基础 URL{{ .Site.BaseURL }}
.Site.LanguageCode语言代码{{ .Site.LanguageCode }}
.Site.Params站点参数{{ .Site.Params.description }}
.Site.Languages语言列表{{ range .Site.Languages }}
.Site.DefaultLanguage默认语言{{ .Site.DefaultLanguage }}

站点方法

方法说明示例
.Site.GetPage获取页面{{ .Site.GetPage "about" }}
.Site.RegularPages所有常规页面{{ range .Site.RegularPages }}
.Site.Pages所有页面{{ range .Site.Pages }}
.Site.Sections所有分区{{ range .Site.Sections }}
.Site.Menus菜单{{ range .Site.Menus.main }}
.Site.Taxonomies分类法{{ range .Site.Taxonomies.tags }}
.Site.Data数据文件{{ range .Site.Data.authors }}

站点方法示例

<!-- 获取特定页面 -->
{{ with .Site.GetPage "about" }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}

<!-- 获取特定路径的页面 -->
{{ with .Site.GetPage "/posts/my-post" }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}

<!-- 遍历所有页面 -->
{{ range .Site.RegularPages }}
<article>
<h2>{{ .Title }}</h2>
</article>
{{ end }}

<!-- 按分区获取页面 -->
{{ range .Site.GetPage "posts" }}
{{ range .Pages }}
<h3>{{ .Title }}</h3>
{{ end }}
{{ end }}

<!-- 使用站点参数 -->
<p>{{ .Site.Params.description }}</p>

<!-- 获取菜单 -->
<nav>
{{ range .Site.Menus.main }}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</nav>

<!-- 获取所有标签 -->
{{ range .Site.Taxonomies.tags }}
<a href="{{ .Page.RelPermalink }}">{{ .Page.Title }} ({{ .Count }})</a>
{{ end }}

404 错误页面模板

404 错误页面是当用户访问不存在的页面时显示的页面。Hugo 会自动在站点根目录生成 404.html 文件。

创建 404 模板

layouts/ 目录下创建 404.html

{{ define "main" }}
<div class="error-page">
<h1>404</h1>
<h2>页面未找到</h2>
<p>抱歉,您访问的页面不存在或已被移除。</p>

<div class="actions">
<a href="{{ .Site.Home.RelPermalink }}" class="btn">
返回首页
</a>
<a href="{{ "posts/" | relURL }}" class="btn-secondary">
浏览文章
</a>
</div>

<div class="search">
<h3>搜索内容</h3>
<form action="{{ "search/" | relURL }}" method="get">
<input type="text" name="q" placeholder="输入关键词...">
<button type="submit">搜索</button>
</form>
</div>
</div>
{{ end }}

多语言 404 页面

对于多语言站点,可以为每种语言创建独立的 404 模板:

layouts/
├── 404.html # 默认
├── 404.zh.html # 中文
└── 404.en.html # 英文

服务器配置

生成 404 页面后,需要在托管平台配置重定向:

平台配置方式
GitHub Pages自动重定向,无需配置
Netlify自动重定向,无需配置
Vercel自动重定向,无需配置
Nginxerror_page 404 /404.html;
ApacheErrorDocument 404 /404.html

RSS 订阅模板

Hugo 内置 RSS 模板,会自动为首页、分区和分类页面生成 RSS 订阅源。

RSS 配置

hugo.toml 中配置 RSS 输出:

[outputs]
home = ['html', 'rss']
section = ['html', 'rss']
taxonomy = ['html', 'rss']
term = ['html', 'rss']

[services]
[services.rss]
limit = 20 # 每个 feed 的最大条目数

禁用 RSS

如果不需要 RSS,可以完全禁用:

disableKinds = ['rss']

自定义 RSS 模板

覆盖 Hugo 的内置 RSS 模板,创建自己的模板:

layouts/
├── home.rss.xml # 首页 RSS
├── section.rss.xml # 分区 RSS
├── taxonomy.rss.xml # 分类 RSS
└── term.rss.xml # 标签 RSS

自定义首页 RSS 模板 layouts/home.rss.xml

{{- $pctx := . -}}
{{- if .IsHome -}}{{- $pctx = .Site -}}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Site.Title }}</title>
<link>{{ .Permalink }}</link>
<description>{{ .Site.Params.description | default .Summary }}</description>
<language>{{ .Site.LanguageCode }}</language>
<managingEditor>{{ .Site.Params.author.email }} ({{ .Site.Params.author.name }})</managingEditor>
<webMaster>{{ .Site.Params.author.email }} ({{ .Site.Params.author.name }})</webMaster>
{{ with .Site.Params.author.email }}<lastBuildDate>{{ now.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</lastBuildDate>{{ end }}
{{ with .OutputFormats.Get "RSS" }}
<atom:link href="{{ .Permalink }}" rel="self" type="application/rss+xml" />
{{ end }}
{{ range $pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</pubDate>
<author>{{ with .Params.author.email }}{{ . }}{{ end }} ({{ with .Params.author.name }}{{ . }}{{ end }})</author>
<guid isPermaLink="true">{{ .Permalink }}</guid>
<description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
</item>
{{ end }}
</channel>
</rss>

在页面中引用 RSS

在模板的 <head> 中添加 RSS 链接:

{{ with .OutputFormats.Get "rss" }}
{{ printf `<link rel=%q type=%q href=%q title=%q>` .Rel .MediaType.Type .Permalink site.Title | safeHTML }}
{{ end }}

渲染结果:

<link rel="alternate" type="application/rss+xml" href="https://example.org/index.xml" title="我的博客">

RSS 配置示例

完整的站点配置示例:

baseURL = 'https://example.org/'
title = '我的博客'

[params]
description = '分享技术与生活'

[params.author]
name = '张三'
email = '[email protected]'

[outputs]
home = ['html', 'rss']
section = ['html', 'rss']
taxonomy = ['html']
term = ['html']

[services]
[services.rss]
limit = 20

copyright = '© 2024 我的博客'

Sitemap 站点地图

Hugo 内置 Sitemap 生成功能,会自动创建 sitemap.xml 文件供搜索引擎使用。

默认行为

构建站点时,Hugo 会在 public/ 目录下生成 sitemap.xml

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.org/</loc>
<lastmod>2024-01-15T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://example.org/posts/first-post/</loc>
<lastmod>2024-01-14T00:00:00+00:00</lastmod>
</url>
</urlset>

Sitemap 配置

hugo.toml 中配置站点地图:

[sitemap]
changefreq = 'weekly' # 更新频率:always, hourly, daily, weekly, monthly, yearly, never
filename = 'sitemap.xml' # 文件名
priority = 0.5 # 默认优先级 (0.0-1.0)

在 Front Matter 中设置

可以单独为每篇内容设置 Sitemap 属性:

---
title: "我的文章"
sitemap:
changefreq: monthly
priority: 0.8
---

禁用特定页面的 Sitemap

---
title: "私有页面"
sitemap:
disable: true
---

自定义 Sitemap 模板

创建 layouts/sitemap.xml 覆盖默认模板:

{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
{{ range .Data.Pages }}
<url>
<loc>{{ .Permalink }}</loc>
{{ if not .Lastmod.IsZero }}
<lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>
{{ end }}
{{ with .Sitemap.ChangeFreq }}
<changefreq>{{ . }}</changefreq>
{{ end }}
{{ if ge .Sitemap.Priority 0.0 }}
<priority>{{ .Sitemap.Priority }}</priority>
{{ end }}
{{ if .IsTranslated }}
{{ range .Translations }}
<xhtml:link
rel="alternate"
hreflang="{{ .Language.Lang }}"
href="{{ .Permalink }}"
/>
{{ end }}
{{ end }}
</url>
{{ end }}
</urlset>

多语言 Sitemap

对于多语言站点,Hugo 会生成包含所有语言版本的站点地图,并使用 xhtml:link 元素标注语言版本。

robots.txt 模板

Hugo 可以自动生成 robots.txt 文件,控制搜索引擎爬虫的行为。

启用 robots.txt 生成

enableRobotsTXT = true

自定义 robots.txt 模板

创建 layouts/robots.txt

User-agent: *
Allow: /

# 禁止访问特定目录
Disallow: /private/
Disallow: /draft/

# Sitemap 位置
Sitemap: {{ .Site.BaseURL }}sitemap.xml

# 特定爬虫规则
User-agent: GPTBot
Disallow: /

多环境配置

针对不同环境生成不同的 robots.txt:

{{- if eq hugo.Environment "production" -}}
User-agent: *
Allow: /
Sitemap: {{ .Site.BaseURL }}sitemap.xml
{{- else -}}
User-agent: *
Disallow: /
{{- end -}}

这样开发环境和预览环境会禁止搜索引擎索引。

小结

本章介绍了 Hugo 模板系统的核心知识:

  1. 变量、条件、循环是模板的基础
  2. Partials 用于复用模板片段
  3. Base Template 定义页面基础结构
  4. 列表模板和单页模板是最常用的模板类型
  5. Taxonomy 模板用于分类页面
  6. Page 和 Site 对象提供了丰富的属性和方法
  7. 404 模板用于错误页面
  8. RSS 模板用于生成订阅源
  9. Sitemap 和 robots.txt 用于 SEO 优化

下一章将学习短代码,这是在 Markdown 中插入复杂内容的方式。