Sorted navigation menu with Jekyll and Liquid

24,131

Solution 1

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

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

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

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

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
---
Share:
24,131
flyx
Author by

flyx

Author of NimYAML and AdaYAML. Mostly here for answering YAML questions.

Updated on August 24, 2020

Comments

  • flyx
    flyx almost 4 years

    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.

  • kikito
    kikito over 12 years
    lol. I guess you can trim that down by compressing everything into one single line of code if that is a concern. Unfortunately liquid doesn't have a {%- %} prefix to collapse empty lines like erb.
  • flyx
    flyx almost 12 years
    Nice one! Still a hack, but much simpler than the other answers.
  • Christiaan
    Christiaan over 11 years
    Be careful with this if you push your site to github pages. For some reason the ordering will be messed up then. See also: github.com/plusjade/jekyll-bootstrap/issues/…
  • Paul Wagland
    Paul Wagland about 11 years
    Just a pity that you can't use custom plugins with GitHub pages… :-\
  • Markus Amalthea Magnuson
    Markus Amalthea Magnuson almost 11 years
    Just an addition: Replacing (1..10) with (1..site.pages.size) makes this loop as short as possible, and will work regardless of how many pages you have. Thanks for a stupid yet highly clever hack :)
  • kikito
    kikito almost 11 years
    @MarkusAmaltheaMagnuson the (1..10) on this code represent possible weights. It could be replaced by (1..MAX_WEIGHT) to make it a bit more clear (and have MAX_WEIGHT defined somewhere else, like in a constants file).
  • MisterMetaphor
    MisterMetaphor almost 11 years
    Nice, thanks for sharing. Just a little addition: in case not all of your items have the specified property, you can alter the sort_by! call to ignore those items: sorted_collection.sort_by! { |i| i.to_liquid[@attributes['sort_by']] || 0 } (replace 0 with infinity if you want it the other way around).
  • sdmeyers
    sdmeyers over 10 years
    Suddenly this started throwing errors on _post pages: Liquid Exception: comparison of Hash with Hash failed in _posts/...
  • Adam B
    Adam B over 10 years
    You good sir deserve a medal. I was just about to resort to generating my site locally and pushing the static HTML to GitHub to be able to use plugins to do this.
  • user1020853
    user1020853 about 10 years
    This worked for me, except that the "active" class needed to go on the <li> instead of the <a>
  • brenna
    brenna about 10 years
    Would this work for sorting by full file path? i.e. {% assign sorted_pages = site.pages | sort:"path" %} and 00-directory/00-file.md would come before 01-anotherDir/00-anotherFile.md
  • brenna
    brenna about 10 years
    It does work! Thanks so much! I've been fighting with this for days.
  • groundh0g
    groundh0g about 10 years
    This worked like a charm for me. If you're on GitHub Pages, this is the answer you're looking for. Of course, I only have 5 nav items, so it's probably overkill to solve with code change. I have two variants of the same site, though. My OCD insisted that their link orders match. :P
  • lfk
    lfk about 10 years
    This is the cleanest solution; there's just a slight error -- the sort key should be given as a string, i.e. sort: 'weight'. Updated the sample code.
  • lfk
    lfk about 10 years
    The aforementioned issue is mentioned and resolved here. It might take a while before the versions running GitHub pages are updated, though.
  • destan
    destan about 10 years
    Hi @Wojtek sort:"weight" just works for me, fyi. By the way thanks for the excellent solution.
  • Paul Ferrett
    Paul Ferrett about 10 years
    This is one of the best things about Jekyll/static-generation - "ugly" as it may be, it only runs once which doesn't affect the user experience or server load. Nice solution!
  • jupiteror
    jupiteror almost 10 years
    @WingLeong I didn't do any tests but that's what worked out for me.
  • wedi
    wedi almost 10 years
    Sorting by weight does work! See answer below Maybe that was changed in Jekyll.
  • eyettea
    eyettea over 9 years
    does not work for me (Jekyll 2.4.0). I defined the weight property in the pages as said above, but the sort seems to ignore it.
  • David Jacquel
    David Jacquel over 9 years
    @eyetea you are right. We need to make an assignment first. I've editer my code and it works on Jekyll 2.4.0. ;-)
  • eyettea
    eyettea over 9 years
    Thanks for the help. I also edited the code and removed the second sort filter, since it looks like it's not needed anymore.
  • David Jacquel
    David Jacquel over 9 years
    You're right. I've edited it myself because your suggested edit was rejected by 3 users ???
  • eyettea
    eyettea over 9 years
    strange, I just deleted the "| sort: weight"... Why would it be rejected? Anyway, problem solved.
  • Dominik
    Dominik over 9 years
    I get an error in 2.4.0: Error: comparison of Jekyll::Page with Jekyll::Page failed .... ?
  • Dominik
    Dominik over 9 years
    mh turns out you can't use decimal numbers like 2.1 as weight... :) have to put them into quotes. Thanks though