Jekyll 创建导航的多种方法

如果你的 Jekyll 站点有很多页面,你可能会希望为这些页面创建导航。
与其手动硬编码导航链接,不如通过程序化方式获取页面列表,为站点生成导航。

虽然 Jekyll 的其他文档已经介绍过如何使用数据文件,但本教程会更深入地讲解如何为站点构建更强大的导航系统。

在 Jekyll 站点中,获取页面主要有两种方式:

下面的示例会从一个基础导航场景开始,并逐步加入更复杂的元素,以展示不同的页面获取方式。每个场景都会包含以下 3 个部分:

_data 目录中的 YAML 文件名为 samplelist.yml

场景如下:

场景 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>

结果

ACME 文档

在这些虚构示例的结果中,实际链接值会被手动替换为 #(以避免 404 错误)。

当你使用 for 循环时,需要自己定义循环中每个元素的变量名。 你定义的变量(这里是 item)会成为访问列表项属性的方式。 使用点表示法来获取项目属性(例如 item.url)。

这里的 YAML 内容主要涉及两种格式:

docs_list_title: ACME Documentation 是一个映射。 你可以通过 site.data.samplelist.docs_list_title 来获取它的值。

docs: 是一个列表。 列表中的每一项都以连字符(-)开头。与映射不同,你通常不会直接访问列表属性。

如果你想访问列表中的某一项,需要指定它在列表中的位置,类似数组索引。例如:

site.data.samplelist.docs[0]

上面的写法会获取列表中的第一项。不过这种方式并不常用。

通常会使用 for 循环遍历列表,并对每一项执行操作。

在导航菜单中,通常会根据 HTML 主题的导航结构,把每个列表项插入到 li 标签中。

每个连字符(-)都表示列表中的一个新项目。 这个示例中的每个项目只有两个属性:

你可以为每个项目添加任意数量的属性。 属性的顺序并不重要。

场景 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>

结果

  1. 配置
  2. 部署
  3. 介绍

现在这些项目会按字母顺序排列。 Liquid 过滤器中的 sort 是基于 title 属性进行排序的,而 title 是列表中的实际属性。

如果 title 不是有效属性,就需要改用其他属性排序。

更多过滤器选项可以参考 Liquid 数组过滤器

注意:你不能直接这样写:

{% for item in site.data.samplelist.docs | sort: "title" %}{% endfor %}

你必须先使用 assigncapture 标签,将 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

分组 2

分组 3

在这个示例中,分组 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

分组 1

分组 2

分组 3

在这个示例中,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.urlpage.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

由于 Deploymentversion2,因此它被排除了。

场景 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 属性:

每个页面的 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>

结果如下:

Getting Started

如果你正在搭建知识库,并且每个分类下有大量主题内容,而且每个分类都有自己的页面,那么这种方式会很有用。

但如果你想按照分类对内容进行排序,并在对应分类名称下进行分组,而不是手动硬编码分类名称,那么你可以使用两个过滤器:

下面的代码可以将页面列表按分类标题进行分组:

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 %}

结果

Getting-started

Configuration

Deployment

下面我们来逐步解析这段代码。首先,我们将集合内容(site.docs)赋值给一个变量(mydocs)。

group_by 过滤器会根据 category 对集合内容进行分组。更具体地说,group_by 会把 mydocs 转换成一个带有 nameitemssize 属性的数组,大致如下:

[
  {"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,并获取其 titleurl 来生成列表链接。

关于 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 属性,并递归渲染嵌套列表。

结果