Hugo content 目录的页面类型与组织方式

核心结论

Hugo 的 content 目录不是简单的 Markdown 文件仓库,而是一棵 Page Tree。目录层级和特殊文件名会决定页面的 KindSectionType、Bundle 类型、模板选择、资源归属以及默认 URL。

设计 content 结构时,不应只考虑最终 URL,而要先判断当前节点是一个具体内容页,还是一个可以包含其他页面的集合。

1
2
3
4
5
具体页面、没有子页面
    → 普通 Markdown 文件或 index.md

集合页面、允许包含子页面
    → _index.md

Hugo 的五种 Page Kind

Kind 表示页面在站点内容模型中的结构角色。它主要由文件位置和 Hugo 内置机制决定,不是一个通常应在 Front Matter 中随意指定的展示参数。

Kind典型内容路径含义常用模板类型
homecontent/_index.md站点根页面home.html
pagecontent/about.mdcontent/about/index.md普通内容页single.htmlpage.html
sectioncontent/post/content/post/_index.md内容集合section.htmllist.html
taxonomycontent/tags/_index.md某个 Taxonomy 的 Term 总览taxonomy.htmllist.html
termcontent/tags/hugo/_index.md某个具体 Term 的内容列表term.htmllist.html

在模板中可以通过以下字段检查当前页面:

1
2
3
4
5
6
Kind={{ .Kind }}
Type={{ .Type }}
Section={{ .Section }}
Layout={{ .Layout }}
BundleType={{ .BundleType }}
RelPermalink={{ .RelPermalink }}

普通文件、index.md_index.md

这三种形式最容易混淆。

普通 Markdown 文件

1
content/about.md

它表示一个普通页面,适合没有同目录资源的简单内容。

1
2
Kind       = page
BundleType = 空

index.md:Leaf Bundle

1
2
3
4
content/about/
├── index.md
├── avatar.jpg
└── resume.pdf

index.md 表示当前目录是一篇具体页面,同时让同目录图片、视频和附件成为该页面的 Page Resources。

1
2
Kind       = page
BundleType = leaf

模板可以通过 .Resources 获取这些资源:

1
2
3
{{ with .Resources.Get "avatar.jpg" }}
    <img src="{{ .RelPermalink }}" alt="Avatar">
{{ end }}

Leaf Bundle 是内容树的叶子,不应该再包含需要独立渲染的子页面。目录中的其他 Markdown 文件通常会成为 Page Resource,而不是独立 Page。

_index.md:Branch Bundle

1
2
3
4
content/docs/
├── _index.md
├── install.md
└── configuration.md

_index.md 表示当前目录是一个集合节点,可以拥有子页面和子 Section。

1
2
Kind       = section
BundleType = branch

可以把两者记成:

1
2
3
4
5
index.md
    = 当前目录就是一篇内容,到这里结束

_index.md
    = 当前目录是一个分支,下面还可以挂其他内容

Section 如何形成

content 下的顶层内容目录天然是 Root Section。例如:

1
2
3
4
5
6
7
content/post/
├── Go/
│   └── defer.md
├── Linux/
│   └── 零拷贝技术.md
└── C++/
    └── lambda.md

这些文章的 Root Section 都是 post

1
2
Section = post
Type    = post

GoLinuxC++ 等中间目录如果没有 _index.md,通常只是文件组织和 URL 路径的一部分,不是具有独立列表页的 Section。如果需要让某个目录成为嵌套 Section,应为它创建 _index.md

1
content/post/Go/_index.md

此时 /post/go/ 才具有明确的 Section Page,并能拥有自己的标题、描述、菜单和子页面集合。

Kind、Type、Section 与 Layout 的区别

这几个概念属于不同维度。

字段回答的问题主要来源
Kind这是普通页、Section 还是 Taxonomy?内容树结构和 Hugo 内置机制
Section这个页面属于哪个 Root Section?content 下的顶层 Section
Type应优先使用哪一组类型模板?Front Matter 的 type,否则通常来自 Root Section
Layout在当前 Type 中选择哪个具体模板?Front Matter 的 layout
SlugURL 最后一个路径段是什么?Front Matter 或文件名

例如 Stack 的搜索页:

1
content/page/search/index.md

Front Matter:

1
2
3
4
5
6
7
---
title: 搜索
layout: search
outputs:
  - html
  - json
---

对应的核心属性为:

1
2
3
Kind   = page
Type   = page
Layout = search

于是可以匹配 Stack 的:

1
2
layouts/page/search.html
layouts/page/search.json

如果移动页面后 Type 不再符合主题约定,可以在 Front Matter 中显式设置:

1
2
type: page
layout: search

layout 不是全局模板开关,它会与 KindType、语言和 Output Format 一起参与 Template Lookup。

每类 Markdown 文件的 Kind、Type 和 Section

KindTypeSection 不是三个需要在每篇 Front Matter 中同时手工填写的字段。Hugo 会先根据内容路径和内置页面类型确定 KindSection,再根据 Front Matter 或 Root Section 推导 Type

可以按照下面的顺序理解:

  1. Hugo 先判断当前文件是否对应首页、普通页面、Section、Taxonomy 根页或 Term 页,由此得到 Kind
  2. Hugo 根据页面所在的顶层内容目录确定 Section;首页没有 Section,Taxonomy 页面使用 Taxonomy 的 plural 名称。
  3. 如果 Front Matter 显式设置了 type,Hugo将其作为 Type;否则普通内容通常继承 Root Section 名称,根目录普通页和首页默认使用 page,Taxonomy 与 Term 默认使用 Taxonomy 的 plural 名称。
  4. layoutslugurloutputsmenu 不负责设置这三个字段,它们分别影响模板选择、发布地址、输出格式和菜单。

当前站点主要文件的实际值如下:

Markdown 文件页面角色KindTypeSection
content/_index.md站点首页homepage空字符串
content/page/about/index.md关于我普通页面pagepagepage
content/page/archives/index.md归档普通页面pagepagepage
content/page/search/index.md搜索普通页面pagepagepage
content/tags/_index.mdTag Taxonomy 根页taxonomytagstags
content/categories/_index.mdCategory Taxonomy 根页taxonomycategoriescategories
content/post/Misc/Hugo-content目录的页面类型与组织方式.md普通博客文章pagepostpost
content/post/Hugo/example/index.mdLeaf Bundle 形式的博客文章pagepostpost
content/post/_index.mdpost Root Section 元信息sectionpostpost
content/post/Go/_index.mdpost 下的嵌套 Section 元信息sectionpostpost
content/tags/hugo/_index.mdhugo Tag Term 元信息termtagstags
content/categories/misc/_index.mdmisc Category Term 元信息termcategoriescategories

表中 content/post/Hugo/example/index.mdcontent/post/_index.mdcontent/post/Go/_index.mdcontent/tags/hugo/_index.mdcontent/categories/misc/_index.md 用于说明对应结构,当前站点不一定已经创建这些具体示例文件。

首页

content 根目录的 _index.md 是 Hugo 站点首页的元信息文件,因此其属性为:

1
2
3
Kind    = home
Type    = page
Section = ""

这里的 _index.md 不会得到 Kind=section,因为 content 根节点在 Hugo 页面模型中具有特殊的首页身份。它当前设置的 menu 只负责把首页加入主菜单,不影响 KindTypeSection

关于我

“关于我”使用 content/page/about/index.md,因此 content/page/about/ 是一个 Leaf Bundle,当前节点是一篇具体页面:

1
2
3
4
Kind    = page
Type    = page
Section = page
Layout  = about

Kind=page 来自 index.md 的 Leaf Page 语义,Section=page 来自顶层目录 content/page。由于 Front Matter 没有显式设置 typeType 默认继承 Root Section,因此也是 page

当前 Front Matter 中的字段分别负责:

1
2
3
4
layout: about # 在 Type=page 的候选模板中优先查找 about 布局
slug: about # 配合 page permalink 生成 /about/
toc: false # 关闭当前页面的 TOC
menu: # 将页面加入主菜单

这些字段都不会改变 Kind=pageType=pageSection=page

归档

归档页同样是位于 page Root Section 下的 Leaf Page,content/page/archives/index.md

1
2
3
4
Kind    = page
Type    = page
Section = page
Layout  = archives

layout: archives 使它选择 Stack 的归档布局,归档数据由该布局读取并组织。页面仍然是普通 page,并不存在 archives 这种 Hugo 内置 Kind。

slug: archives 只参与 /archives/ URL 的生成,不会把 TypeSection 设置为 archives

搜索

搜索页 content/page/search/index.md 也是 Leaf Page:

1
2
3
4
Kind    = page
Type    = page
Section = page
Layout  = search

它的特殊之处不在 Kind,而在自定义布局和 Output Format:

1
2
3
4
layout: search
outputs:
  - html
  - json

layout: search 使 HTML 输出匹配 Stack 的 layouts/page/search.html,JSON 输出匹配对应的搜索数据模板。outputs 只决定同一个 Page 需要生成哪些输出格式,不会创建新的 Kind,也不会改变 TypeSection

Tag Taxonomy 根页

Hugo 默认配置中存在 tag = "tags",因此逻辑路径 /tags 被识别为 Tag Taxonomy 根节点 content/tags/_index.md

1
2
3
4
Kind    = taxonomy
Type    = tags
Section = tags
Layout  = tags

这里的 Kind=taxonomy 不是单独由 _index.md 决定的,而是由“启用了 plural 名称为 tags 的 Taxonomy”与“文件位于 content/tags/_index.md”共同决定。_index.md 为 Hugo 自动生成的 Taxonomy Page 补充标题、菜单等元信息。

如果没有这个文件,只要 Tag Taxonomy 已启用并且文章使用了 tags,Hugo 仍然可以生成 /tags/。缺少的只是自定义标题、菜单和其他页面元信息。

Category Taxonomy 根页

Hugo 默认配置中存在 category = "categories",因此该文件对应 Category Taxonomy 根页面 content/categories/_index.md

1
2
3
4
Kind    = taxonomy
Type    = categories
Section = categories
Layout  = categories

TypeSection 使用 plural 名称 categories,而不是 singular 名称 category。文章 Front Matter 也使用 plural 字段建立关系:

1
2
categories:
  - Misc

当前文件中的 layout: categoriesslug: categoriesmenu 都不会创建 Taxonomy;真正让页面获得 Kind=taxonomy 的是 Hugo Taxonomy 配置和逻辑路径。

普通博客文章

普通博客文章自身位于:

1
content/post/.../*.md

它是普通 Markdown 内容文件,并且位于 post Root Section 下,因此:

1
2
3
Kind    = page
Type    = post
Section = post

中间目录 Misc 不会把 TypeSection 改为 Misc。它当前只是文件组织和 URL 逻辑路径的一部分;文章的 categories: [Misc] 是独立的 Taxonomy 关系,也不会改变 Section

普通文件与 Leaf Bundle 在这三个值上可以完全相同:

1
2
content/post/Hugo/example.md
content/post/Hugo/example/index.md

二者通常都是:

1
2
3
Kind    = page
Type    = post
Section = post

区别在于后者是 Leaf Bundle,可以把同目录图片和附件作为 Page Resources 管理,而不是 KindTypeSection 不同。

post 目录下的 _index.md

如果创建:

1
content/post/_index.md

它会成为 post Root Section 的元信息页面:

1
2
3
Kind    = section
Type    = post
Section = post

如果进一步创建:

1
content/post/Go/_index.md

Go 会成为嵌套 Section,但 .Section 仍然返回 Root Section post

1
2
3
Kind    = section
Type    = post
Section = post

需要获得当前嵌套 Section Page 时应使用 .CurrentSection,不能只依赖 .Section 区分 post/Gopost/Linux

Tag Term 页

如果为具体 Tag 创建元信息文件:

1
content/tags/hugo/_index.md

它对应 /tags/hugo/,属性为:

1
2
3
Kind    = term
Type    = tags
Section = tags

Kind=term 表示它是一个具体 Term 的文章集合,.Pages 通常是所有声明了 tags: [Hugo] 的文章。TypeSection 仍然使用 Taxonomy plural 名称 tags,而不是 Term 名称 hugo

Category Term 采用相同规则:

1
2
3
4
5
content/categories/misc/_index.md

Kind    = term
Type    = categories
Section = categories

Front Matter 能修改哪些值

三个字段的可控方式并不相同:

字段能否通过 Front Matter 修改正确控制方式
Kind不能直接修改调整文件形态、内容路径,或者启用相应 Hugo 内置机制
Type可以设置 type;未设置时由 Root Section 等信息推导
Section不能直接修改调整文件所在的 Root Section 或使用 Hugo Module mount

例如在“关于我”页面设置:

1
type: landing

结果会变成:

1
2
3
Kind    = page
Type    = landing
Section = page

只有 Type 被覆盖,页面仍然是普通 Page,也仍然位于 page Section。

类似地,下面这些字段都不能改变 Page Kind:

1
2
3
layout: taxonomy
slug: tags
url: /tags/

它们可以改变模板候选或发布 URL,但不能把 content/page/tags/index.md 转换为 Taxonomy Page。

把 Taxonomy 文件移动到 page 后会发生什么

正确位置:

1
2
3
4
5
content/tags/_index.md

Kind    = taxonomy
Type    = tags
Section = tags

如果移动并改成:

1
2
3
4
5
content/page/tags/index.md

Kind    = page
Type    = page
Section = page

如果移动后仍使用 _index.md

1
2
3
4
5
content/page/tags/_index.md

Kind    = section
Type    = page
Section = page

即使通过 slugurl 或 permalink 让它们最终发布到 /tags/,它们也不会恢复 Kind=taxonomy。与此同时,Hugo 仍会为真正的 Tag Taxonomy 生成 /tags/,从而产生重复输出路径。因此 Taxonomy 元信息文件应保持在 content/tags/_index.mdcontent/categories/_index.md

在模板中验证实际值

判断页面类型时应直接查看 Hugo Page 对象,而不是根据浏览器地址猜测。可以临时在模板中输出:

1
2
3
4
5
6
7
8
<pre>
Kind={{ .Kind }}
Type={{ .Type }}
Section={{ printf "%q" .Section }}
Layout={{ .Layout }}
BundleType={{ .BundleType }}
RelPermalink={{ .RelPermalink }}
</pre>

其中 printf "%q" 可以明确显示首页的空 Section。当前站点还可以使用 hugo list all 查看内容文件的 kindsection 和最终 permalink,再结合 .Type 的推导规则定位模板匹配问题。

搜索页为什么必须使用 index.md

正确结构是:

1
content/page/search/index.md

如果误写成:

1
content/page/search/_index.md

搜索节点会从普通页面变成 Section:

1
2
Kind=page       → Kind=section
single 模板语义 → list 模板语义

Stack 的搜索功能依赖 layouts/page/search.htmllayouts/page/search.json,还需要加载 search.tsx。一旦页面变成 Section,模板查找方向就会改变,最终可能只得到空白列表页或无法生成正确的 JSON 搜索索引。

Taxonomy 页面为什么使用 _index.md

Hugo 默认提供:

1
2
3
[taxonomies]
category = "categories"
tag = "tags"

文章通过 plural 字段关联 Term:

1
2
3
4
5
categories:
  - 博客
tags:
  - Hugo
  - Stack

Hugo 会自动生成:

1
2
3
4
5
/categories/
/categories/博客/
/tags/
/tags/hugo/
/tags/stack/

对应的可选元信息文件是:

1
2
3
4
5
6
7
8
content/categories/_index.md
    → 分类 Taxonomy 根页

content/tags/_index.md
    → 标签 Taxonomy 根页

content/tags/hugo/_index.md
    → Hugo Term 页

这些节点都是集合,所以使用 _index.md。文件负责补充标题、描述、封面和菜单,真正的文章归属关系来自每篇文章的 Front Matter。

如果把 content/tags/_index.md 写成 content/tags/index.md,就会把 /tags/ 声明成普通叶子 Page,与 Hugo 自动生成的 Taxonomy 根页发生内容模型冲突。

内容路径与最终 URL 可以分离

内容文件所在目录不会绝对决定最终 URL。Hugo 还会考虑 slugurl 和站点的 permalinks 配置。

当前站点使用:

1
2
3
[permalinks]
post = "/p/:slug/"
page = "/:slug/"

因此:

1
2
3
4
5
content/post/Linux/零拷贝技术.md
    → /p/零拷贝技术/

content/page/search/index.md
    → /search/

这说明目录主要用于表达内容模型和模板类型,而 permalink 可以独立控制发布路径。不要为了缩短 URL 随意移动内容目录,应优先使用 permalink 或 url 配置。

Stack 主题下的推荐结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
content/
├── _index.md
├── post/
│   ├── _index.md                  # 可选,post Section 元信息
│   ├── Go/
│   │   └── defer.md
│   └── Hugo/
│       └── page-bundle/
│           ├── index.md
│           └── cover.png
├── page/
│   ├── about/
│   │   └── index.md
│   ├── archives/
│   │   └── index.md
│   └── search/
│       └── index.md
├── categories/
│   └── _index.md
└── tags/
    ├── _index.md
    └── hugo/
        └── _index.md

这里的职责划分是:

路径职责
content/post首页、归档和 RSS 中的主要文章
content/page搜索、关于、归档等独立功能页
content/categoriesCategory Taxonomy 元信息
content/tagsTag Taxonomy 和 Term 元信息

Stack 通过 mainSections = ["post"] 识别主要文章,因此普通功能页不会混入首页文章列表和搜索索引。

组织页面时的判断顺序

新增页面前可以依次判断:

  1. 这是具体内容还是集合?具体内容使用 foo.mdindex.md,集合使用 _index.md
  2. 是否需要与页面绑定图片和附件?需要时使用 Leaf Bundle,即 目录/index.md
  3. 是否需要子页面和独立列表页?需要时使用 Branch Bundle,即 目录/_index.md
  4. 它属于哪个业务类型?博客文章放在 post,功能页放在 page,不要只根据 URL 选择目录。
  5. 是否属于 Taxonomy?标签和分类关系写入文章 Front Matter,content/tagscontent/categories 只保存集合元信息。
  6. 主题模板位于哪个 Type 目录?例如 layouts/page/search.html 要求搜索页以 Type=page 参与查找。
  7. 最终 URL 是否符合需求?不符合时优先调整 permalink、slugurl,不要先破坏内容模型。

常见错误

错误后果正确方式
_index.md 创建普通功能页页面变成 Section,使用 list 语义改为 index.md
index.md 创建标签根页普通 Page 与 Taxonomy 根页冲突改为 tags/_index.md
为了修改 URL 随意移动顶层目录Type 和模板匹配可能改变配置 permalink 或显式 type
认为 layout 可以脱离 Type 独立工作找不到主题中的类型模板同时检查 KindTypeLayout
在 Leaf Bundle 中放独立 Markdown 子页面子 Markdown 可能变成 Page Resource改用 Branch Bundle
手工维护标签文章列表与 Hugo 自动 Taxonomy 重复在文章 Front Matter 中声明 tags

参考资料

使用 Hugo 构建
主题 StackJimmy 设计