65.9K
CodeProject 正在变化。 阅读更多。
Home

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

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015年8月11日

CPOL

6分钟阅读

viewsIcon

8643

使用 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/

texturl 属性稍后将用于构建菜单中实际的 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>

这里有很多魔法在发生,所以我们将一步一步地进行。

  1. 首先,我们将当前页面的 URL 保存到一个名为 `navurl` 的变量中,去掉末尾的 `index.html`。

     {% assign navurl = page.url | remove: 'index.html' %}
    

    所以当当前页面是 `/first-menu/index.html` 时,`navurl` 将被设置为 `/first-menu/`。

  2. 然后,我们遍历最顶层的菜单项。
    对于上面示例中的数据文件,这些项是:

    • Home
    • 一级菜单
    • 二级菜单

    我们稍后再处理子项。

  3. 下一行显示菜单项的链接。
    我们在这里使用第 1 步中的 `navurl` 变量,来确定当前循环的菜单项的 URL 是否等于当前页面的 URL。
    如果是,我们将链接显示为粗体,因为它指向当前页面。

     <a href="{{ item.url }}">
         {% if item.url == navurl %}
             <b>{{ item.text }}</b>
         {% else %}
             {{ item.text }}
         {% endif %}
     </a>
  4. 菜单项是否有子项?如果有,我们现在需要决定是否显示它们。
    有两种情况必须显示它们:

    • 当菜单项是当前页面时
    • 当菜单项是当前页面的父级时

    当所有子项都放置在其父项的子文件夹中时,这很容易实现(见上文)。
    因为那时,判断菜单项是当前页面还是当前页面的父级,就变成了一个简单的 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 %}` 的意思是:
    每当当前页面的 URLnavurl包含“First menu”的 URLitem.url时,我们就为“First menu”显示子项。

    → 当当前页面是上面示例中的三个中的任何一个时,每个页面的 URL 都包含 `/first-menu/`,因此在所有三种情况下都会显示“First menu”的子项。

  5. 我们通过再次包含 `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 上找到完整的代码示例。
通常,我会忽略包含生成站点的文件夹,但在这种情况下,我也提交了它,这样您就可以看到结果,而无需先构建。

© . All rights reserved.