nasauber.de

Blog

Dynamic nested menu with Jekyll

Whilst porting my old graduating class's homepage abi2002amschiller.de to Jekyll, I faced a problem on how to implement the menu. It's a dynamic menu with nested submenus, without a defined level count. The old PHP implementation always showed a "path" which leads to the the currently opened menu, and the actual menu itself.

Doing this in Jekyll/Liquid was a bit tricky. In Jekyll's Navigation Tutorial, one can find a Nested tree navigation with recursion, but this always dumps the whole menu. Not what I wanted. After some research and coding I ended up getting the very menu I had before, with the help of a data file and three scripts partly called recursively, all in pure Liquid. Not too hard, but also not quite trivial.

I publish this stuff as CC0 1.0. Do what you want with it. Here we are:

The menu definition

The data file navigation.yml consists of menu elements with names and links each, and possibly a sub-menu. There's one root element holding the top-level menu. Menus can be nested arbitrarily deep:

- name: "Home"
  link: "/"

  submenu:

  - name: "Top level entry 1"
    link: "/1/"

    submenu:

    - name: "Submenu 1 entry 1"
      link: "/1/1/"

    - name: "Submenu 1 entry 2"
      link: "/1/2/"

      submenu:

      - name: "Subsubmenu 1.2 entry 1"
        link: "/1/2/1/"

      - name: "Subsubmenu 1.2 entry 2"
        link: "/1/2/2/"

      - name: "Subsubmenu 1.2 entry 3"
        link: "/1/2/3/"

    - name: "Submenu 1 entry 3"
      link: "/1/3/"

  - name: "Top level entry 2"
    link: "/2/"

  - name: "Top level entry 3"
    link: "/3/"

The menu generation code

The menu itself consists of two unordered lists. One is the "path" to the currently opened menu, the other one is the menu itself. I indented the code here for better readability here. For the output linked below, there's no indentation and empty lines, because I really have a rough time each time I mess with Liquid's white space control for nice HTML output ;-)

Here's the main menu code navigation.html:

<nav id="navigation_path">
<ul>
{% include navigation_path.html menu = site.data.navigation %}
</ul>
</nav>

<nav id="navigation_menu">
<ul>
{% include navigation_find_menu.html menu = site.data.navigation %}

{% unless open_menu %}
{% assign open_menu = site.data.navigation[0].submenu %}
{% endunless %}

{% for item in open_menu %}
    {% if page.dir == item.link %}
        <li><span>{{ item.name }}</span></li>
    {% else %}
        <li><a href="{{ item.link }}">{{ item.name }}</a></li>
    {% endif %}
{% endfor %}
</ul>
</nav>

Here's navigation_path.html. This one walks along the menu structure and recursively includes itself until the path is built, always passing itself a part of the original menu data:

{% for item in include.menu %}
    {% if page.dir contains item.link %}
        {% if item.submenu %}
            {% if page.dir == item.link %}
                <li><span>{{ item.name }}</span></li>
            {% else %}
                <li><a href="{{ item.link }}">{{ item.name }}</a></li>
            {% endif %}
        {% endif %}
    {% endif %}

    {% if item.submenu %}
        {% include navigation_path.html menu = item.submenu %}
    {% endif %}
{% endfor %}

And here's navigation_find_menu.html, which – also by including itself recursively – finds the currently opened menu. If no open menu is found (because we're on the first level), open_menu stays empty. In this case, site.data.navigation[0].submenu, the top-level submenu of the root element, is displayed by navigation.html.

{% for item in include.menu %}
    {% if page.dir == item.link %}
        {% if item.submenu %}
            {% assign open_menu = item.submenu %}
        {% else %}
            {% assign open_menu = include.menu %}
        {% endif %}
    {% endif %}

    {% if item.submenu %}
        {% include navigation_find_menu.html menu = item.submenu %}
    {% endif %}
{% endfor %}

What it looks like

You can view the output of the above code, using a minimal layout. Only the links have been converted to relative ones manually to make it work with a non-"/" root. You can as well download the sources.

I hope this helps somebody :-)