Liquid navigation tree generator This include was written for a Jekyll site I'm building. It generates nested HTML unordered lists to a given depth dynamically by parsing page urls. It works fine, but the code feels messy and it takes Jekyll upwards of 20 seconds to build the site (up from ~3 seconds before this navigation was implemented).

Basically the code goes through each page in the site, makes sure the page is at the right depth by examining how many levels are represented in the url (separated by depth), and outputs a link to the page if the page is indeed at the right length and its url contains the base url of its parent page.

nav.html include:

{% comment %} Init call with current_depth, max_depth. Both are zero-based. {% endcomment %}

{% assign current_lvl_url_length = include.current_depth | plus: 2 %}
{% assign next_lvl_url_length = current_lvl_url_length | plus: 1 %}
{% assign max_url_length = include.max_depth | plus: 2 %}
{% assign base_url = include.base_url %}

{% if include.base_url == nil %}
    {% assign base_url = '/' %}
    {{ base_url }}
{% endif %}

{% for node in site.pages %}
    {% assign node_url_parts = node.url | split: '/' %}
    {% assign filename = node_url_parts | last %}

    {% if node_url_parts.size == current_lvl_url_length and node.url contains base_url and filename != 'index.html' and filename contains 'html' %}
        &lt;li class="page-link"&gt;
            &lt;a href='{{node.url}}'&gt;{{node.title}}&lt;/a&gt;

            {% if next_lvl_url_length &lt;= max_url_length %}
                {% assign node_base_url = node.url | replace: '.html' | append: '/' %}
                {% assign is_submenu = false %}
                {% for subnode in site.pages %}
                    {% assign subnode_url_parts = subnode.url | split: '/' %}
                    {% if subnode_url_parts.size == next_lvl_url_length and subnode.url contains node_base_url %}
                    {% assign is_submenu = true %}
                    {% break %}
                    {% endif %}
                {% endfor %}

                {% if is_submenu %}
                    &lt;ul&gt;
                        {% assign next_depth = include.current_depth | plus: 1 %}
                        {% include nav.html current_depth=next_depth max_depth=include.max_depth base_url=node_base_url old_base_url=base_url %}

                        {% comment %} Reverse recursion state: {% endcomment %}

                        {% assign current_lvl_url_length = current_lvl_url_length | minus: 1 %}
                        {% assign next_lvl_url_length = next_lvl_url_length | minus: 1 %}
                        {% assign base_url = include.old_base_url %}
                        {% if include.old_base_url == nil %}
                            {% assign base_url = '/' %}
                        {% endif %}
                    &lt;/ul&gt;
                {% endif %}
            {% endif %}

        &lt;/li&gt;
    {% endif %}
{% endfor %}


header.html where nav.html is initially called:

&lt;header class="site-header"&gt;

  &lt;div class="wrapper"&gt;

    &lt;a class="site-title" href="{{ site.baseurl }}/"&gt;{{ site.title }}&lt;/a&gt;

    &lt;nav class="site-nav"&gt;
      &lt;a href="#" class="menu-icon"&gt;
        &lt;svg viewBox="0 0 18 15"&gt;
          &lt;path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/&gt;
          &lt;path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/&gt;
          &lt;path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/&gt;
        &lt;/svg&gt;
      &lt;/a&gt;

      &lt;ul&gt;{% include nav.html current_depth=0 max_depth=2 %}&lt;/ul&gt;
    &lt;/nav&gt;

  &lt;/div&gt;

&lt;/header&gt;
Put a lot of work into refactoring this code, trying to maximize for readability and simplicity in an effort to increase maintainability. I'm posting my results here since I know creating recursive, dynamic, no-plugin navigation is an issue Jekyll users often face, and some are likely to land here searching for a solution.

nav.html

{% comment %} Init call with base_url (optional) and max_depth. {% endcomment %}

{% if lvl_base_url == nil %}
  {% assign lvl_base_url = '/' %}
  {% if include.base_url %}
    {% assign lvl_base_url = include.base_url %}
  {% endif %}
{% endif %}

{% assign current_lvl_url_length = lvl_base_url | split: '/' | size %}
{% if lvl_base_url == '/' %}
  {% assign current_lvl_url_length = 1 %}
{% endif %}

{% capture links %}
  {% for node in site.pages %}
    {% assign node_url_length = node.url | split: '/' | size | minus: 1 %}
    {% assign filename = node.url | split: '/' | last %}
    {% if node_url_length == current_lvl_url_length and node.url contains lvl_base_url and filename != 'index.html' and filename contains 'html' %}
      &lt;li class="page-link"&gt;
        &lt;a href='{{ node.url }}'&gt;{{ node.title }}&lt;/a&gt;
        {% if include.max_depth &gt; current_lvl_url_length | plus: 1 %}
          {% assign lvl_base_url = node.url | replace: '.html' | append: '/' %}

          {% comment %} Include can access parent state; hence no variables passed explicitly: {% endcomment %}
          {% include nav.html %}

          {% comment %} Reverse state changes made by child: {% endcomment %}
          {% assign current_lvl_url_length = current_lvl_url_length | minus: 1 %}
          {% assign directory_to_remove = lvl_base_url | split: '/' | last | append: '/' %}
          {% assign lvl_base_url = lvl_base_url | replace: directory_to_remove %}
        {% endif %}
      &lt;/li&gt;
    {% endif %}
  {% endfor %}
{% endcapture %}

{% assign links = links | strip %}

{% if links.size &gt; 0 %}
&lt;ul&gt;{{ links }}&lt;/ul&gt;
{% endif %}


header.html [example usage]

&lt;header class="site-header"&gt;

  &lt;div class="wrapper"&gt;

    &lt;a class="site-title" href="{{ site.baseurl }}/"&gt;{{ site.title }}&lt;/a&gt;

    &lt;nav class="site-nav"&gt;
      &lt;a href="#" class="menu-icon"&gt;
        &lt;svg viewBox="0 0 18 15"&gt;
          &lt;path fill="#424242"
                d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/&gt;
          &lt;path fill="#424242"
                d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/&gt;
          &lt;path fill="#424242"
                d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/&gt;
        &lt;/svg&gt;
      &lt;/a&gt;

      {% if page.url contains '/academy' %}
        {% include nav.html base_url='/academy/' max_depth=3 %}
      {% elsif page.url contains '/college' %}
        {% include nav.html base_url='/college/' max_depth=3 %}
      {% else %}
        {% include nav.html max_depth=2 %}
      {% endif %}

    &lt;/nav&gt;

  &lt;/div&gt;

&lt;/header&gt;