模板系统
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 custom | Front Matter 中设置的 layout |
| Page kinds | home、section、taxonomy、term、page |
| Layouts standard 1 | list 或 single |
| Output format | 输出格式:html、rss、json 等 |
| Layouts standard 2 | all(通用布局) |
| Language | 语言代码:en、zh 等 |
| Media type | 媒体类型:html、xml 等 |
命名示例:
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 循环中使用 break 和 continue 语句:
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 |
15 | 24小时制小时 | 14 |
03 | 12小时制小时 | 02 |
04 | 分钟 | 30 |
05 | 秒 | 45 |
.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 }}
errorf 和 warnf 对于调试和验证很有用。使用 erroridf 和 warnidf 可以通过配置抑制特定警告:
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 的对比
| 特性 | 标准 Partial | Partial 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+)
新系统使用统一的模板查找逻辑,标识符优先级如下:
- Layout custom - Front Matter 中设置的
layout - Page kinds -
home、section、taxonomy、term、page - Layouts standard 1 -
list或single - Output format - 输出格式(
html、rss等) - Layouts standard 2 -
all(通用布局) - Language - 语言代码
- Media type - 媒体类型
- Page path - 页面路径
- Type - Front Matter 中的
type
查找示例:
对于一个位于 /posts/my-post/ 的页面:
layouts/posts/page.html- 路径特定模板layouts/posts/mylayout.html- 自定义 layout(如果设置了layout: mylayout)layouts/page.html- 通用单页模板layouts/all.html- 通用模板- 主题中的对应模板
旧模板系统(v0.145 及之前)
旧的查找顺序:
layouts/<section>/single.htmllayouts/<section>/list.htmllayouts/_default/single.htmllayouts/_default/list.htmlthemes/<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 }} |
.Aliases | URL 别名 | {{ range .Aliases }}{{ . }}{{ end }} |
URL 和路径
| 方法 | 说明 | 示例 |
|---|---|---|
.Permalink | 绝对 URL | {{ .Permalink }} |
.RelPermalink | 相对 URL | {{ .RelPermalink }} |
.Slug | URL 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 }} |
.GitInfo | Git 信息 | {{ 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 | 自动重定向,无需配置 |
| Nginx | error_page 404 /404.html; |
| Apache | ErrorDocument 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 模板系统的核心知识:
- 变量、条件、循环是模板的基础
- Partials 用于复用模板片段
- Base Template 定义页面基础结构
- 列表模板和单页模板是最常用的模板类型
- Taxonomy 模板用于分类页面
- Page 和 Site 对象提供了丰富的属性和方法
- 404 模板用于错误页面
- RSS 模板用于生成订阅源
- Sitemap 和 robots.txt 用于 SEO 优化
下一章将学习短代码,这是在 Markdown 中插入复杂内容的方式。