使用 Jekyll 构建伪动态树形菜单





0/5 (0投票)
使用 Jekyll 构建伪动态树形菜单
我正在将一个现有网站迁移到 Jekyll。现有网站是用 CMS 构建的,CMS 生成了一个动态树形菜单。想象一下这样的站点地图:
- Home
- 一级菜单
- 一级菜单 (子项)
- 一级菜单 (子子项)
- 一级菜单 (子项)
- 二级菜单
- 二级菜单 (子项)
加载网站时(“主页”),只显示一级菜单项。子项不可见,当前页面的链接是粗体显示的。
- Home
- 一级菜单
- 二级菜单
点击其中一个有子项的链接时,会显示下一级子项。所以点击“一级菜单”后,看起来是这样的:
- Home
- 一级菜单
- 一级菜单 (子项)
- 二级菜单
……然后点击“一级菜单 (子项)”
- Home
- 一级菜单
- 一级菜单 (子项)
- 一级菜单 (子子项)
- 一级菜单 (子项)
- 二级菜单
……以此类推。
这就是我们将在本文中使用 Jekyll 构建的内容。
初步思考
如果我们稍加思考,就会发现我们需要的菜单实际上并非“动态”。
Jekyll 无论如何都会为每个页面生成一个独立的 HTML 文件,如果我们孤立地查看某个页面,那么该页面的菜单中没有任何动态内容。
当然,当前页面的链接需要加粗等等,但运行时没有任何内容需要在该页面上改变——每次加载页面时,菜单看起来都应该完全一样。
所以我们只需要生成静态页面,但每个页面上的菜单“视图”有所不同。
规则如下:
- 如果链接是当前页面的链接,则需要将其显示为粗体。
- 通常,我们不需要显示子项……但有两个例外:
- 我们需要显示当前页面的子项(仅下一级,即使有多级子项)
- 如果当前页面本身是一个子项,我们需要从最顶层一直显示到该页面的子项
将站点地图保存在数据文件中
首先,我们将使用一个 YAML 数据文件来存储完整的站点地图:菜单项、它们的 URL 和它们的子项。
/_data/menu.yml
- text: Home
url: /
- text: First menu
url: /first-menu/
subitems:
- text: First menu (sub)
url: /first-menu/first-menu-sub/
subitems:
- text: First menu (sub-sub)
url: /first-menu/first-menu-sub/first-menu-sub-sub/
- text: Second menu
url: /second-menu/
subitems:
- text: Second menu (sub)
url: /second-menu/second-menu-sub/
text
和 url
属性稍后将用于构建菜单中实际的 HTML 链接。
所有 URL(/
、/first-menu/
、/first-menu/first-menu-sub/
等)都需要作为文件夹存在于 Jekyll 的源文件夹中,每个文件夹内有一个 `index.html` 或 `index.md` 文件。
/index.html
/first-menu/index.html
/first-menu/first-menu-sub/index.html
etc.
在此示例中,我将为所有这些文件使用最基本的内容。
---
layout: default
title: whatever
---
为什么?
技术上来说,也可以这样做:
- text: First menu
url: /first-menu/
subitems:
- text: First menu (sub)
url: /somewhere-else/first-menu-sub/
subitems:
- text: First menu (sub-sub)
url: /first-menu-sub-sub/
有了这个文件夹结构,生成基本菜单也可以工作。
但是,我们稍后将使用的某些技巧(让菜单看起来像动态的)将不再奏效。
基本解决方案
在我这种特殊情况(转换一个具有现有菜单结构的现有网站)下,我知道树形嵌套子项的深度,所以我可以用几个嵌套循环来构建它(这里的“几个”等于菜单项的最大嵌套级别)。
当我第一次尝试这样做时,我将规则的代码(“将当前页面的链接显示为粗体”等)放入了一个 include 文件,以避免在每个循环中重复。
但我仍然喜欢“与嵌套级别无关”的解决方案,所以我又在网上搜索了一下,阅读了一些教程,发现 Jekyll 可以递归嵌套 include 文件。
换句话说:一个 include 文件可以包含它自身。
基于此,我提出了以下解决方案。
首先,是 布局文件 (/_layouts/default.html)
<!DOCTYPE html>
<html>
<head>
<title>{{ page.title }}</title>
</head>
<body>
<h2>Navigation:</h2>
{% include nav.html nav=site.data.menu %}
<hr>
<h1>{{ page.title }}</h1>
{{ content }}
</body>
</html>
这里没什么特别的——它只是显示导航(通过 include 文件),然后一条水平线,然后是实际的页面内容。
唯一值得一提的是,我们将完整站点地图从数据文件传递给了 include 文件。
{% include nav.html nav=site.data.menu %}
根据 文档,它可以通过 `{{ include.nav }}` 在 include 文件内部访问。
递归 include 文件
这里就是所有魔法发生的地方!
/_includes/nav.html
{% assign navurl = page.url | remove: 'index.html' %}
<ul>
{% for item in include.nav %}
<li>
<a href="{{ item.url }}">
{% if item.url == navurl %}
<b>{{ item.text }}</b>
{% else %}
{{ item.text }}
{% endif %}
</a>
</li>
{% if item.subitems and navurl contains item.url %}
{% include nav.html nav=item.subitems %}
{% endif %}
{% endfor %}
</ul>
这里有很多魔法在发生,所以我们将一步一步地进行。
-
首先,我们将当前页面的 URL 保存到一个名为 `navurl` 的变量中,去掉末尾的 `index.html`。
{% assign navurl = page.url | remove: 'index.html' %}
所以当当前页面是 `/first-menu/index.html` 时,`navurl` 将被设置为 `/first-menu/`。
-
然后,我们遍历最顶层的菜单项。
对于上面示例中的数据文件,这些项是:- Home
- 一级菜单
- 二级菜单
我们稍后再处理子项。
-
下一行显示菜单项的链接。
我们在这里使用第 1 步中的 `navurl` 变量,来确定当前循环的菜单项的 URL 是否等于当前页面的 URL。
如果是,我们将链接显示为粗体,因为它指向当前页面。<a href="{{ item.url }}"> {% if item.url == navurl %} <b>{{ item.text }}</b> {% else %} {{ item.text }} {% endif %} </a>
-
菜单项是否有子项?如果有,我们现在需要决定是否显示它们。
有两种情况必须显示它们:- 当菜单项是当前页面时
- 当菜单项是当前页面的父级时
当所有子项都放置在其父项的子文件夹中时,这很容易实现(见上文)。
因为那时,判断菜单项是当前页面还是当前页面的父级,就变成了一个简单的 URL 比较。{% if item.subitems and navurl contains item.url %} <!-- show subitems here --> {% endif %}
为了清楚起见,我将再次展示数据文件中的相关部分。
- text: First menu url: /first-menu/ subitems: - text: First menu (sub) url: /first-menu/first-menu-sub/ subitems: - text: First menu (sub-sub) url: /first-menu/first-menu-sub/first-menu-sub-sub/
当我们在遍历第一级菜单项时,其中一个项是“First menu”,URL 为:
/first-menu/
。现在 `{% if item.subitems and navurl contains item.url %}` 的意思是:
每当当前页面的 URL(navurl
)包含“First menu”的 URL(item.url
)时,我们就为“First menu”显示子项。→ 当当前页面是上面示例中的三个中的任何一个时,每个页面的 URL 都包含 `/first-menu/`,因此在所有三种情况下都会显示“First menu”的子项。
-
我们通过再次包含 `nav.html` 来显示子项,这次我们传递的是当前菜单项的子项。
{% include nav.html nav=item.subitems %}
所以它会再次执行我刚才描述的所有操作,只是在菜单中深入了一级:它会遍历刚刚传递的子项,如果子项本身也有子项,它会再次包含自身,以此类推。
就是这样!
注意事项
在转换我的网站时,我发现有一个例子不完全适用于我刚才描述的方法:当指向根目录 ` / ` 的Home链接有子项时。
问题是,所有 URL 都包含根目录的 URL (`/`),这意味着根目录的子项将始终显示,无论当前页面是什么。
我通过将Home菜单项的 URL 改为 `/home/` 来解决这个问题。
- text: Home
url: /home/
subitems:
- text: Introduction
url: /home/introduction/
这解决了Home的子项始终显示的问题。
然后,我只是在根文件夹中放了一个空的 `index.html` 文件(带有一个 meta refresh 到 `/home/`),这样任何访问 `/` 的人都会被重定向到 `/home/`。
我不确定这对 SEO 来说是否是最佳解决方案,但对我来说已经足够了,因为该网站流量不大……而且将来可能也不会很大。
示例代码
您可以在 Bitbucket 上找到完整的代码示例。
通常,我会忽略包含生成站点的文件夹,但在这种情况下,我也提交了它,这样您就可以看到结果,而无需先构建。