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 :-)