Jekyll 创建导航的多种方法
如果你的 Jekyll 站点有很多页面,你可能会希望为这些页面创建导航。
与其手动硬编码导航链接,不如通过程序化方式获取页面列表,为站点生成导航。
虽然 Jekyll 的其他文档已经介绍过如何使用数据文件,但本教程会更深入地讲解如何为站点构建更强大的导航系统。
在 Jekyll 站点中,获取页面主要有两种方式:
-
获取 YAML 数据源中列出的页面。
将页面数据存储在_data文件夹中的 YAML(或 JSON、CSV)文件里,循环读取 YAML 属性,并将值插入到主题中。 -
通过遍历页面 front matter 获取页面。
检查页面 front matter 中的特定属性,筛选符合条件的页面,并将这些页面的 front matter 值插入到主题中。
下面的示例会从一个基础导航场景开始,并逐步加入更复杂的元素,以展示不同的页面获取方式。每个场景都会包含以下 3 个部分:
- YAML 配置
- Liquid 模板代码
- Result 输出结果
_data 目录中的 YAML 文件名为 samplelist.yml。
场景如下:
- 场景 1:基础列表
- 场景 2:排序列表
- 场景 3:两级导航列表
- 场景 4:三级导航列表
- 场景 5:使用页面变量选择 YAML 列表
- 场景 6:为当前页面添加 active 类
- 场景 7:按条件包含项目
- 场景 8:根据 Front Matter 属性获取内容
- 场景 9:使用递归实现嵌套树形导航
场景 1:基础列表
你希望返回一个基础的页面列表。
YAML
docs_list_title: ACME 文档
docs:
- title: 介绍
url: introduction.html
- title: 配置
url: configuration.html
- title: 部署
url: deployment.html
Liquid
<h2>{{ site.data.samplelist.docs_list_title }}</h2>
<ul>
{% for item in site.data.samplelist.docs %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
结果
在这些虚构示例的结果中,实际链接值会被手动替换为 #(以避免 404 错误)。
当你使用 for 循环时,需要自己定义循环中每个元素的变量名。
你定义的变量(这里是 item)会成为访问列表项属性的方式。
使用点表示法来获取项目属性(例如 item.url)。
这里的 YAML 内容主要涉及两种格式:
- 映射(mapping)
- 列表(list)
docs_list_title: ACME Documentation 是一个映射。
你可以通过 site.data.samplelist.docs_list_title 来获取它的值。
docs: 是一个列表。
列表中的每一项都以连字符(-)开头。与映射不同,你通常不会直接访问列表属性。
如果你想访问列表中的某一项,需要指定它在列表中的位置,类似数组索引。例如:
site.data.samplelist.docs[0]
上面的写法会获取列表中的第一项。不过这种方式并不常用。
通常会使用 for 循环遍历列表,并对每一项执行操作。
在导航菜单中,通常会根据 HTML 主题的导航结构,把每个列表项插入到 li 标签中。
每个连字符(-)都表示列表中的一个新项目。
这个示例中的每个项目只有两个属性:
titleurl
你可以为每个项目添加任意数量的属性。 属性的顺序并不重要。
场景 2:排序列表
假设你希望按照 title 对列表进行排序。
可以先将 docs 集合赋值给一个变量,然后对变量应用 Liquid 的 sort 过滤器:
Liquid
{% assign doclist = site.data.samplelist.docs | sort: 'title' %}
<ol>
{% for item in doclist %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ol>
结果
现在这些项目会按字母顺序排列。
Liquid 过滤器中的 sort 是基于 title 属性进行排序的,而 title 是列表中的实际属性。
如果 title 不是有效属性,就需要改用其他属性排序。
更多过滤器选项可以参考 Liquid 数组过滤器。
注意:你不能直接这样写:
{% for item in site.data.samplelist.docs | sort: "title" %}{% endfor %}
你必须先使用 assign 或 capture 标签,将 site.data.samplelist.docs 转换为变量。
场景 3:两级导航列表
假设你想创建一个更复杂的列表,包含多个标题分组和子项目。
为此,需要在每个列表项中增加一个额外层级来存储这些信息:
YAML
toc:
- title: 分组 1
subfolderitems:
- page: 项目 1
url: /thing1.html
- page: 项目 2
url: /thing2.html
- page: 项目 3
url: /thing3.html
- title: 分组 2
subfolderitems:
- page: 内容 1
url: /piece1.html
- page: 内容 2
url: /piece2.html
- page: 内容 3
url: /piece3.html
- title: 分组 3
subfolderitems:
- page: 部件 1
url: /widget1.html
- page: 部件 2
url: /widget2.html
- page: 部件 3
url: /widget3.html
Liquid
{% for item in site.data.samplelist.toc %}
<h3>{{ item.title }}</h3>
<ul>
{% for entry in item.subfolderitems %}
<li><a href="{{ entry.url }}">{{ entry.page }}</a></li>
{% endfor %}
</ul>
{% endfor %}
结果
在这个示例中,分组 1 是第一个列表项。
在该列表项内部,它的子页面作为一个属性存在,而这个属性本身又包含一个列表(subfolderitems)。
Liquid 代码首先通过 for item in site.data.samplelist.toc 遍历第一层,
然后通过 for entry in item.subfolderitems 遍历第二层属性。
和 item 一样,entry 也只是循环变量名,可以随意命名。
场景 4:三级导航列表
在上一节基础上,我们再增加一层深度(subsubfolderitems)。
这里的格式会更复杂,但原理是相同的。
YAML
toc2:
- title: 分组 1
subfolderitems:
- page: 项目 1
url: /thing1.html
- page: 项目 2
url: /thing2.html
subsubfolderitems:
- page: 子项目 1
url: /subthing1.html
- page: 子项目 2
url: /subthing2.html
- page: 项目 3
url: /thing3.html
- title: 分组 2
subfolderitems:
- page: 内容 1
url: /piece1.html
- page: 内容 2
url: /piece2.html
- page: 内容 3
url: /piece3.html
subsubfolderitems:
- page: 子内容 1
url: /subpiece1.html
- page: 子内容2
url: /subpiece2.html
- title: 分组 3
subfolderitems:
- page: 部件 1
url: /widget1.html
subsubfolderitems:
- page: 子部件 1
url: /subwidget1.html
- page: 子部件 2
url: /subwidget2.html
- page: 部件 2
url: /widget2.html
- page: 部件 3
url: /widget3.html
Liquid
<div>
{% if site.data.samplelist.toc2[0] %}
{% for item in site.data.samplelist.toc2 %}
<h3>{{ item.title }}</h3>
{% if item.subfolderitems[0] %}
<ul>
{% for entry in item.subfolderitems %}
<li><a href="{{ entry.url }}">{{ entry.page }}</a>
{% if entry.subsubfolderitems[0] %}
<ul>
{% for subentry in entry.subsubfolderitems %}
<li><a href="{{ subentry.url }}">{{ subentry.page }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
{% endif %}
</div>
Result
在这个示例中,if site.data.samplelist.toc2[0] 用于确保该 YAML 层级实际包含数据。
如果 [0] 位置没有内容,就可以跳过这一层。
专业提示:对齐 for 循环和 if 语句
为了让代码更清晰,请将 Liquid 标签的开始和结束位置对齐,例如 for 循环和 if 语句。这样可以更容易看出哪些标签已经闭合。如果代码会显示在 Markdown 页面中,请让 HTML 开始和结束标签贴紧左边缘,否则 Markdown 过滤器可能会把内容识别为代码块。如有需要,可以将整个代码示例包裹在 div 标签中,以确保代码被 HTML 标签包围。
场景 5:使用页面变量选择 YAML 列表
假设你的侧边栏会根据不同文档集而变化。 例如你的网站有 3 个不同产品,因此希望每个产品都有独立的侧边栏。
你可以在页面 front matter 中存储侧边栏列表名称,然后动态传入对应列表。
页面 front matter
---
title: 我的页面
sidebar: toc
---
Liquid
<ul>
{% for item in site.data.samplelist[page.sidebar] %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
Result
在这个场景中,我们希望将页面 front matter 的值传递给包含变量的 for 循环。
当赋值变量不是字符串,而是数据引用时,必须使用方括号(而不是花括号)来引用 front matter 的值。
更多信息请参考 Liquid 文档中的 Expressions and Variables。
方括号通常用于点表示法无法使用的场景。 你也可以阅读这个 Stack Overflow 回答 了解更多细节。
场景 6:为当前页面添加 active 类
除了从 YAML 数据文件中插入列表项之外,通常还需要在用户当前访问的页面上高亮对应链接。
可以通过在匹配当前页面 URL 的项目上添加 active 类来实现。
CSS
.result li.active a {
color: lightgray;
cursor: default;
}
Liquid
{% for item in site.data.samplelist.docs %}
<li class="{% if item.url == page.url %}active{% endif %}">
<a href="{{ item.url }}">{{ item.title }}</a>
</li>
{% endfor %}
Result
这里假设当前页面是 Deployment。
为了确保 YAML 文件中的 item.url 与 page.url 匹配,可以在页面中输出 {{ page.url }} 进行检查。
场景 7:按条件包含项目
你可能希望按条件包含列表项。 例如,你的网站可能有多个输出版本,而某些侧边栏项目只想在特定版本中显示。
可以在每个列表项中添加属性,然后根据这些属性有条件地显示内容。
YAML
docs2_list_title: ACME Documentation
docs2:
- title: 介绍
url: introduction.html
version: 1
- title: 配置
url: configuration.html
version: 1
- title: 部署
url: deployment.html
version: 2
Liquid
<ul>
{% for item in site.data.samplelist.docs2 %}
{% if item.version == 1 %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endif %}
{% endfor %}
</ul>
Result
由于 Deployment 的 version 为 2,因此它被排除了。
场景 8:根据 Front Matter 属性获取内容
如果你不想把导航项存放在 _data 文件夹中的 YAML 文件里,你可以使用 for 循环遍历每个页面或集合(collection)的 front matter,并根据 front matter 中的属性获取内容。
在这个场景中,假设我们有一个名为 _docs 的集合。相比普通页面,集合通常更适合这种场景,因为它可以让你缩小遍历范围。(尽量避免遍历大量内容,否则会增加构建时间。Collections 可以帮助你缩小范围。)
在我们的示例中,docs 集合中有 6 个文档:Sample 1、Sample 2、Topic 1、Topic 2、Widget 1 和 Widget 2。
集合中的每个文档至少包含以下 3 个 front matter 属性:
titlecategoryorder
每个页面的 front matter 如下(为了简洁这里合并展示):
---
Title: Sample 1
category: getting-started
order: 1
---
---
Title: Sample 2
category: getting-started
order: 2
---
---
Title: Topic 1
category: configuration
order: 1
---
---
Title: Topic 2
category: configuration
order: 2
---
---
Title: Widget 1
category: deployment
order: 1
---
---
Title: Widget 2
category: deployment
order: 2
---
注意,虽然在文档的 front matter 中使用了 category,但它并不像文章(posts)中的 category 那样属于内置变量。换句话说,你不能直接通过 site.docs.category 来访问它。
如果你只是想获取集合中特定分类下的所有文档,可以使用带有 if 条件判断的 for 循环来检查指定分类:
<h3>Getting Started</h3>
<ul>
{% for doc in site.docs %}
{% if doc.category == "getting-started" %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endif %}
{% endfor %}
</ul>
结果如下:
如果你正在搭建知识库,并且每个分类下有大量主题内容,而且每个分类都有自己的页面,那么这种方式会很有用。
但如果你想按照分类对内容进行排序,并在对应分类名称下进行分组,而不是手动硬编码分类名称,那么你可以使用两个过滤器:
group_bysort
下面的代码可以将页面列表按分类标题进行分组:
Liquid
{% assign mydocs = site.docs | group_by: 'category' %}
{% for cat in mydocs %}
<h2>{{ cat.name | capitalize }}</h2>
<ul>
{% assign items = cat.items | sort: 'order' %}
{% for item in items %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
{% endfor %}
结果
下面我们来逐步解析这段代码。首先,我们将集合内容(site.docs)赋值给一个变量(mydocs)。
group_by 过滤器会根据 category 对集合内容进行分组。更具体地说,group_by 会把 mydocs 转换成一个带有 name、items 和 size 属性的数组,大致如下:
[
{"name": "getting-started", "items": [Sample 1, Sample 2],"size": 2},
{"name": "configuration", "items": [Topic 1, Topic 2], "size": 2},
{"name": "deployment", "items": [Widget 1, Widget 2], "size": 2}
]
通过 for cat in mydocs,我们遍历 mydocs 数组中的每个项目,并输出分类名称 name。
获取分类名称后,我们再为文档创建变量 items,并使用 sort 过滤器按照 order 属性排序。这里使用 cat.items 点语法,是因为我们正在访问 items 数组中的内容。sort 过滤器会按照数字从小到大进行升序排列。
for item in items 循环会遍历每一个 item,并获取其 title 和 url 来生成列表链接。
关于 group_by 过滤器的更多细节,请参阅 Jekyll 的 Templates 文档 以及 这篇 Siteleaf 教程。关于 sort 过滤器的更多细节,请参阅 Liquid 文档中的 sort。
无论你是通过文档 front matter 中的属性来获取页面,还是使用 YAML 数据文件,这两种方式都可以让你以编程方式为网站构建更强大的导航系统。
场景 9:使用递归实现嵌套树形导航
假设你想实现一个支持任意层级深度的树形嵌套导航。我们可以通过递归遍历导航链接树来实现。
YAML
nav:
- title: Deployment
url: deployment.html
subnav:
- title: Heroku
url: heroku.html
subnav:
- title: Jekyll on Heroku
url: jekyll-on-heroku.html
- title: Help
url: help.html
Liquid
首先,我们创建一个用于渲染导航树的 include 文件。该文件命名为 _includes/nav.html
<ul>
{% for item in include.nav %}
<li><a href="{{ item.url }}">{{ item.title }}</a>
{% if item.subnav %}
{% include nav.html nav=item.subnav %}
{% endif %}
</li>
{% endfor %}
</ul>
要在 layout 或页面中渲染导航,只需要 include 这个模板并传入 nav 参数即可。在这个示例中,我们使用 page.nav 从 YAML front matter 中获取导航数据。
{% include nav.html nav=page.nav %}
我们的 include 文件会先读取这些数据,然后检查每个项目是否包含 subnav 属性,并递归渲染嵌套列表。
结果