Sorted navigation menu with Jekyll and Liquid

JekyllLiquid

Jekyll Problem Overview


I'm constructing a static site (no blog) with Jekyll/Liquid. I want it to have an auto-generated navigation menu that lists all existing pages and highlight the current page. The items should be added to the menu in a particular order. Therefore, I define a weight property in the pages' YAML:

---
layout : default
title  : Some title
weight : 5
---

The navigation menu is constructed as follows:

<ul>
  {% for p in site.pages | sort:weight %}
    <li>
      <a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
        {{ p.title }}
      </a>
    </li>
  {% endfor %}
</ul>

This creates links to all existing pages, but they're unsorted, the sort filter seems to be ignored. Obviously, I'm doing something wrong, but I can't figure out what.

Jekyll Solutions


Solution 1 - Jekyll

Since Jekyll 2.2.0 you can sort an array of objects by any object property. You can now do :

{% assign pages = site.pages | sort:"weight"  %}
<ul>
  {% for p in pages %}
    <li>
      <a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
        {{ p.title }}
      </a>
    </li>
  {% endfor %}
</ul>

And save a lot of build time compared to @kikito solution.

edit: You MUST assign your sorting property as an integer weight: 10 and not as a string weight: "10".

Assigning sorting properties as string will ends up in a a string sort like "1, 10, 11, 2, 20, ..."

Solution 2 - Jekyll

Your only option seems to be using a double loop.

<ul>
{% for weight in (1..10) %}
  {% for p in site.pages %}
    {% if p.weight == weight %}
      <li>
        <a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
          {{ p.title }}
        </a>
      </li>
    {% endif %}
  {% endfor %}
{% endfor %}
</ul>

Ugly as it is, it should work. If you also have pages without a weight, you will have to include an additional internal loop just doing {% unless p.weight %} before/after the current internal one.

Solution 3 - Jekyll

Below solution works on Github (doesn't require a plugin):

{% assign sorted_pages = site.pages | sort:"name" %}
{% for node in sorted_pages %}
  <li><a href="{{node.url}}">{{node.title}}</a></li>
{% endfor %}

Above snippet sorts pages by file name (name attribute on Page object is derived from file name). I renamed files to match my desired order: 00-index.md, 01-about.md – and presto! Pages are ordered.

One gotcha is that those number prefixes end up in the URLs, which looks awkward for most pages and is a real problem in with 00-index.html. Permalilnks to the rescue:

---
layout: default
title: News
permalink: "index.html"
---

P.S. I wanted to be clever and add custom attributes just for sorting. Unfortunately custom attributes are not accessible as methods on Page class and thus can't be used for sorting:

{% assign sorted_pages = site.pages | sort:"weight" %} #bummer

Solution 4 - Jekyll

I've written a simple Jekyll plugin to solve this issue:

  1. Copy sorted_for.rb from https://gist.github.com/3765912 to _plugins subdirectory of your Jekyll project:

    module Jekyll
      class SortedForTag < Liquid::For
        def render(context)
          sorted_collection = context[@collection_name].dup
          sorted_collection.sort_by! { |i| i.to_liquid[@attributes['sort_by']] }
    
          sorted_collection_name = "#{@collection_name}_sorted".sub('.', '_')
          context[sorted_collection_name] = sorted_collection
          @collection_name = sorted_collection_name
    
          super
        end
    
        def end_tag
          'endsorted_for'
        end
      end
    end
    
    Liquid::Template.register_tag('sorted_for', Jekyll::SortedForTag)
    
  2. Use tag sorted_for instead of for with sort_by:property parameter to sort by given property. You can also add reversed just like the original for.

  3. Don't forget to use different end tag endsorted_for.

In your case the usage look like this:

<ul>
  {% sorted_for p in site.pages sort_by:weight %}
    <li>
      <a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
        {{ p.title }}
      </a>
    </li>
  {% endsorted_for %}
</ul>

Solution 5 - Jekyll

The simplest solution would be to prefix the filename of your pages with an index like this:

00-home.html 01-services.html 02-page3.html

Pages are be ordered by filename. However, now you'll have ugly urls.

In your yaml front matter sections you can override the generated url by setting the permalink variable.

For instance:

---
layout: default
permalink: index.html
---

Solution 6 - Jekyll

Easy solution:

Assign a sorted array of site.pages first then run a for loop on the array.

Your code will look like:

{% assign links = site.pages | sort: 'weight' %}
{% for p in links %}
  <li>
    <a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
      {{ p.title }}
    </a>
  </li>
{% endfor %}

This works in my navbar _include which is simply:

<section id="navbar">
	<nav>
		{% assign tabs = site.pages | sort: 'weight' %}
		{% for p in tabs %}
			<span class="navitem"><a href="{{ p.url }}">{{ p.title }}</a></span>
		{% endfor %}
	</nav>
</section>

Solution 7 - Jekyll

I've solved this using a generator. The generator iterates over pages, getting the navigation data, sorting it and pushing it back to the site config. From there Liquid can retrieve the data and display it. It also takes care of hiding and showing items.

Consider this page fragment:

---
navigation:
  title: Page name
  weight: 100
  show: true
---
content.

The navigation is rendered with this Liquid fragment:

{% for p in site.navigation %}
<li> 
	<a  {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">{{ p.navigation.title }}</a>
</li>
{% endfor %}

Put the following code in a file in your _plugins folder:

module Jekyll

  class SiteNavigation < Jekyll::Generator
    safe true
    priority :lowest

    def generate(site)

        # First remove all invisible items (default: nil = show in nav)
        sorted = []
        site.pages.each do |page|
          sorted << page if page.data["navigation"]["show"] != false
        end

        # Then sort em according to weight
        sorted = sorted.sort{ |a,b| a.data["navigation"]["weight"] <=> b.data["navigation"]["weight"] } 

        # Debug info.
        puts "Sorted resulting navigation:  (use site.config['sorted_navigation']) "
        sorted.each do |p|
          puts p.inspect 
        end

        # Access this in Liquid using: site.navigation
        site.config["navigation"] = sorted
    end
  end
end

I've spent quite a while figuring this out since I'm quite new to Jekyll and Ruby, so it would be great if anyone can improve on this.

Solution 8 - Jekyll

I can get the code below works on with Jekyll/Liquid match to your requirement with category:

> - creates links to all existing pages, > - sorted by weight (works as well on sorting per category), > - highlight the current page.

On top of them it shows also number of post. All is done without any plug-in.

<ul class="topics">
{% capture tags %}
    {% for tag in site.categories %}
        {{ tag[0] }}
    {% endfor %}
{% endcapture %}
{% assign sortedtags = tags | split:' ' | sort %}
    {% for tag in sortedtags %}
    <li class="topic-header"><b>{{ tag }} ({{ site.categories[tag] | size }} topics)</b>
        <ul class='subnavlist'>
        {% assign posts = site.categories[tag] | sort:"weight" %}
        {% for post in posts %}
            <li class='recipe {% if post.url == page.url %}active{% endif %}'>
    		<a href="/{{ site.github.project_title }}{{ post.url }}">{{ post.title }}</a>
			</li>
        {% endfor %}
        </ul>
    </li>
    {% endfor %}
</ul>

Check it on action on our networking page. You may click a post to highlight the navigation, as well a given link to bring you to the source page where their weight is assigned.

Solution 9 - Jekyll

If you're trying to sort by weight and by tag and limit the number to 10, here's code to do it:

{% assign counter = '0' %}
{% assign pages = site.pages | sort: "weight"  %}
{% for page in pages %}
{% for tag in page.tags %}
{% if tag == "Getting Started" and counter < '9' %}
{% capture counter %}{{ counter | plus:'1' }}{% endcapture %}
<li><a href="{{ page.permalink | prepend: site.baseurl }}">{{page.title}}</a></li>
{% endif %}
{% endfor %}
{% endfor %} 

Solution 10 - Jekyll

The solution above by @kikito also worked for me. I just added a few lines to remove pages without weight from the navigation and to get rid of white space:

<nav>
  <ul>
    {% for weight in (1..5) %}
      {% unless p.weight %}
        {% for p in site.pages %}
          {% if p.weight == weight %}
            {% if p.url == page.url %}
              <li>{{ p.title }}</li>
            {% else %}
              <li><a href="{{ p.url }}" title="{{ p.title }}">{{ p.title }}</a></li>
            {% endif %}
          {% endif %}
        {% endfor %}
      {% endunless %}
    {% endfor %}
  </ul>
</nav>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionflyxView Question on Stackoverflow
Solution 1 - JekyllDavid JacquelView Answer on Stackoverflow
Solution 2 - JekyllkikitoView Answer on Stackoverflow
Solution 3 - JekyllWojtek KruszewskiView Answer on Stackoverflow
Solution 4 - JekyllJan DupalView Answer on Stackoverflow
Solution 5 - JekyllMark MeeusView Answer on Stackoverflow
Solution 6 - JekyllsdmeyersView Answer on Stackoverflow
Solution 7 - JekyllkevinView Answer on Stackoverflow
Solution 8 - JekylleQ19View Answer on Stackoverflow
Solution 9 - JekyllTom JohnsonView Answer on Stackoverflow
Solution 10 - JekylljupiterorView Answer on Stackoverflow