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