Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to sort a collection by front-matter values? #898

Closed
noelforte opened this issue Feb 3, 2020 · 19 comments
Closed

Is it possible to sort a collection by front-matter values? #898

noelforte opened this issue Feb 3, 2020 · 19 comments

Comments

@noelforte
Copy link

Let's say I have a template located at _items/my-awesome-post.md with the following front matter:

---
title: My awesome post
order: 1
---

Content

I've loaded this template into a collection using getFilteredByGlob as referenced in the documentation:

eleventyConfig.addCollection("items", function(collection) {
    return collection.getFilteredByGlob("_items/**/*.md");
});

I'm wondering if it's possible to sort these collection items by the order property in my front-matter via my liquid templates as below or whether there's another way to do it via the liquid template engine or the Eleventy API. I've already tried doing the following and it doesn't appear to return anything.

{% assign items_by_order = collections.items | sort: "order" %}
{% for item in items_by_order %}

I also tried using the sort filter to grab the data but that didn't return anything either {% assign items_by_order = collections.items | sort: data.order %}

@noelforte
Copy link
Author

noelforte commented Feb 4, 2020

Solved it (for now at least)! Ended up using the collections API to sort the array of returned posts, but wasn't sure if there was a way to do it within a template.

eleventy.addCollection('items', function(config) {
    return collection.getFilteredByGlob("_items/**/*.md")
        .sort((a, b) => b.data.order - a.data.order);
});

Additionally in my search I came across #338 which touched on the eleventy data structure some, and also lead me to this area of the documentation: https://www.11ty.dev/docs/collections/#collection-item-data-structure. As someone porting his site over from Jekyll, it was very frustrating to try to figure out what data was available to grab from a returned collection object and how it was namespaced (ie, confusing that item.url isn't namespaced but item.data.title is namespaced.

Is work being currently done to improve the documentation on global variables/data objects available in templates/collections? If so, I'd be happy to contribute where needed to help make their structure clearer.

@paulshryock
Copy link
Contributor

paulshryock commented Feb 4, 2020

Sorting via JavaScript is the best way that I've found. One option is directly in the .addCollection method, as you've done above. This works for collections that will always be ordered by a single property, like order or title.

For collections that might need to be sorted by different properties in different contexts, I think you could sort the collection with JavaScript in a JavaScript template, which ends in .11ty.js. I'm not sure if you can directly access the collections object without somehow require()ing it first... I'll post an update if I find a way to do this.

@ckot
Copy link

ckot commented Apr 22, 2020

Based on how the docs recommend to use collections.foo | reverse rather than collections.foo.reverse(), due to the former mututating the collection, I decided to create a custom filter, and found that it works, and also, subsequent iterations over the collection without this filter use the default ordering (didn't seem to get mutated).

function sortByOrder(values) {
    let vals = [...values];     // this *seems* to prevent collection mutation...
    return vals.sort((a, b) => Math.sign(a.data.order - b.data.order));
}

eleventyConfig.addFilter("sortByOrder", sortByOrder);
  

I like this approach as I can use it on any of my collections, and I have lots of collections, and I don't want have to do a eleventyConfig.addCollection("orderedFoos")... for each of them.

I do admit I'm a bit nervous since docs go into using the collections API for custom sorting rather than suggesting a simple approach such as this. Do others think this approach is inadvisable?

@paulshryock
Copy link
Contributor

I like this approach as I can use it on any of my collections, and I have lots of collections, and I don't want have to do a eleventyConfig.addCollection("orderedFoos")... for each of them.

I do admit I'm a bit nervous since docs go into using the collections API for custom sorting rather than suggesting a simple approach such as this. Do others think this approach is inadvisable?

I like that approach, doing the sorting in Eleventy filters. That makes sense because it's bananas to create new collections for every single sort variation of every single collection. Your filter approach allows any kind of sorting on any kind of collection, as needed. 👍

I think doing the ordering inside the collection creation, as advised in the docs and as I mentioned above, only makes sense when you know a particular collection will always be sorted a particular way.

That's what I think. I'm curious how others are approaching this.

@ckot
Copy link

ckot commented Apr 22, 2020

@paulshryock Glad to hear at least one person doesn't think I'm on crack.

What still makes me nervous is, if the solution really is this simple, why didn't someone suggest something similar months ago? I'm definitely not a rocket scientist - there must be at least some downside to my approach.

@paulshryock
Copy link
Contributor

@paulshryock Glad to hear at least one person doesn't think I'm on crack.

What still makes me nervous is, if the solution really is this simple, why didn't someone suggest something similar months ago? I'm definitely not a rocket scientist - there must be at least some downside to my approach.

It may just be that there are tons of issues, and no one got around to posting a reply about this. 🤷‍♂️

@noelforte
Copy link
Author

@paulshryock yeah, that's probably the most likely, and also that I didn't close my own issue after solving it 😂

@ckot, that's a sweet solve, love the idea of filters as a method of sorting...

i'll close this now since it seems like there's a lot of good ideas but people can keep discussing if they want! cheers, yall!

@spl
Copy link

spl commented Jul 22, 2020

I wanted to sort a bunch of pages, each about a different person, by the name of each person. (This is just to add to what others wrote above in the hope that it might help others like myself who happen up this issue.)

I came up with this function:

function sortByName(values) {
  return values.slice().sort((a, b) => a.data.sortName.localeCompare(b.data.sortName))
}

(I'm no JavaScript expert, but slice() seems like a reasonable alternative to the spread (...) mentioned by @ckot. I'm not sure which is better, but l prefer the syntactic simplicity of .slice().)

I added it to the Eleventy configuration (.eleventy.js) with:

module.exports = (config) => {
  config.addFilter('sortByName', sortByName)
}

And I added a sortName field to the frontmatter of each Markdown document in my people-tagged collection. For example:

---
title: Epictetus
tags: people
sortName: Epictetus
---

To test it, I did this:

<ul>
{%- for person in collections.people | sortByName -%}
  <li><a href="{{ person.url }}">{{ person.data.title }}</a></li>
{%- endfor -%}
</ul>

<ul>
{%- for person in collections.people | sortByName | reverse -%}
  <li><a href="{{ person.url }}">{{ person.data.title }}</a></li>
{%- endfor -%}
</ul>

I saw what I expected, so I guess it worked.

@kentsin
Copy link

kentsin commented Apr 13, 2021

Is it possible to introduce more powe to collection?

For example, for school event calendar purpose, it is simple to use a collection to have all events in the same day. But for a yearly summary of activities, there are needed to have collection that have all activities of the year, then sorted by subject (physic, math, etc.) then by month, and then the class (year 1, year 2, etc.)

Now to perform these, one need to write special sort and filters in js. But that kinds of power is gnerally needed.

@paulshryock
Copy link
Contributor

paulshryock commented Apr 13, 2021

Hi @kentsin, I would create an events collection and sort it by subject, month, and class.

Then I would use an Eleventy filter to show all events from that colection filtered by a specific year.

// .eleventy.js

module.exports = function(eleventyConfig) {
  // Create a collection
  eleventyConfig.addCollection('events', function(collectionApi) {
    return collectionApi
      // Start with markdown templates inside `events`
      .getFilteredByGlob('events/**/*.md')
      // Sort content alphabetically by `subject`
      .sort((a, b) => {
        const subjectA = a.data.subject.toUpperCase()
        const subjectB = b.data.subject.toUpperCase()
        if (subjectA > subjectB) return 1
        if (subjectA < subjectB) return -1
        return 0
      })
      // Sort content ascending by `month` (assuming `month` is a Number)
      .sort((a, b) => Number(a.data.month) - Number(b.data.month))
      // Sort content alphabetically by `class` (assuming `class` is a string like `Year 1`)
      .sort((a, b) => {
        const classA = a.data.class.toUpperCase()
        const classB = b.data.class.toUpperCase()
        if (classA > classB) return 1
        if (classA < classB) return -1
        return 0
      })
  })

  // Create a filter
  eleventyConfig.addFilter('filterByYear', (value, year) => {
    // Filter by `year`
    return Number(value.data.year) === Number(year)
  })
}
// events/my-event.md

---
title: My Event
year: 2021
month: 2
subject: Math
class: Year 1
---
Hello world.
// 2021-events.md

## 2021 events
{% for event in collections.events | filterByYear: 2021 %}
  {% if forloop.first == true %}<ul>{% endif %}
  <li><a href="{{ event.url }}">{{ event.data.title }}</a></li>
  {% if forloop.last == true %}</ul>{% endif %}
{% endfor %}

Eleventy Docs

@iwm-donath
Copy link

iwm-donath commented Apr 28, 2021

Based on how the docs recommend to use collections.foo | reverse rather than collections.foo.reverse(), due to the former mututating the collection, I decided to create a custom filter, and found that it works, and also, subsequent iterations over the collection without this filter use the default ordering (didn't seem to get mutated).

function sortByOrder(values) {
    let vals = [...values];     // this *seems* to prevent collection mutation...
    return vals.sort((a, b) => Math.sign(a.data.order - b.data.order));
}

eleventyConfig.addFilter("sortByOrder", sortByOrder);
  

I like this approach as I can use it on any of my collections, and I have lots of collections, and I don't want have to do a eleventyConfig.addCollection("orderedFoos")... for each of them.

I do admit I'm a bit nervous since docs go into using the collections API for custom sorting rather than suggesting a simple approach such as this. Do others think this approach is inadvisable?

@ckot Would you mind showing a minimal example? I have some trouble getting this to work. I have "order: n" in my frontmatter, but when I use this loop I keep getting illegal tag errors:

{%- for item in collections.test-collection | sortByOrder -%}
  <li>{{ item.data.survey_results }}</li>
{%- endfor -%}

@paulshryock
Copy link
Contributor

paulshryock commented Apr 28, 2021

I have some trouble getting this to work.

@iwm-donath, did you add the quoted code block into your Eleventy config? Your config file would need to return a function which includes that code, so something like this:

// .eleventy.js

module.exports = function (eleventyConfig) {

  // Create the filter function.
  function sortByOrder(values) {
    let vals = [...values]
    return vals.sort((a, b) => Math.sign(a.data.order - b.data.order))
  }

  // Add the filter.
  eleventyConfig.addFilter('sortByOrder', sortByOrder)
}

If you're using Liquid, try skipping the | before the sortByOrder in your template code.

@iwm-donath
Copy link

iwm-donath commented Apr 29, 2021

I have some trouble getting this to work.

@iwm-donath, did you add the quoted code block into your Eleventy config? Your config file would need to return a function which includes that code, so something like this:

// .eleventy.js

module.exports = function (eleventyConfig) {

  // Create the filter function.
  function sortByOrder(values) {
    let vals = [...values]
    return vals.sort((a, b) => Math.sign(a.data.order - b.data.order))
  }

  // Add the filter.
  eleventyConfig.addFilter('sortByOrder', sortByOrder)
}

If you're using Liquid, try skipping the | before the sortByOrder in your template code.

Well, that wasn't my problem, but I feel stupid nonetheless: My test code was inside a md-file. As soon as I put it inside a njk-file it worked 🤦‍♂️ markdownTemplateEngine: "njk" is also useful here ...
Thanks for your help!

@j9t
Copy link

j9t commented Aug 9, 2021

If it’s useful for anyone, I worked with @spl’s solution (thank you!) to come up with the following to sort by front matter titles:

.eleventy.js:

eleventyConfig.addFilter('sortByTitle', values => {
  return values.slice().sort((a, b) => a.data.title.localeCompare(b.data.title))
})

Overview page (here working with venues):

<ul>
  {% set entries = collections.venues %}
  {% for entry in entries | sortByTitle %}
  <li><a href={{ entry.url | url }}>{{ entry.data.title }}</a>
  {% endfor %}
</ul>

(Open for feedback on how to make this simpler and better!)

@danfascia
Copy link

Does the nunjucks sort filter (standard to the language) actually work? I cannot get it to work using the syntax

{% for page in collections.nav | sort(false, false, 'order') %}

I much prefer the versatility of doing this in templating rather than having to create many collections just to serve a single use case. I know I could build custom filters but things like this feel pretty core basic.

@kuwts
Copy link

kuwts commented Aug 11, 2021

Does the nunjucks sort filter (standard to the language) actually work? I cannot get it to work using the syntax

{% for page in collections.nav | sort(false, false, 'order') %}

I much prefer the versatility of doing this in templating rather than having to create many collections just to serve a single use case. I know I could build custom filters but things like this feel pretty core basic.

Agreed, this seems like functionality that is so common that it should be already implemented. I used Kirby before Eleventy and these simple sorting and filtering methods were all readily available (no need to create and customize it yourself). I also have been having some trouble using the sort() method. Simply trying to sort by an int within the front matter of a collection is giving me such a headache.

@pdehaan
Copy link
Contributor

pdehaan commented Aug 11, 2021

Does the nunjucks sort filter (standard to the language) actually work? I cannot get it to work using the syntax

{% for page in collections.nav | sort(false, false, 'order') %}

I got this working, but got some unexpected results if I didn't have an order property defined in one of my templates. 🤷

{% for p in collections.nav | sort(false, false, 'data.order') %}
  <p><a href="{{ p.url | url }}" data-order="{{ p.data.order }}">{{ p.data.title }}</a></p>
{% endfor %}

OUTPUT

<p><a href="/pages/zero/" data-order="0">ZeRo</a></p>
<p><a href="/pages/one/" data-order="1">OnE</a></p>
<p><a href="/pages/two/" data-order="2">TwO</a></p>
<p><a href="/pages/three/" data-order="3">tHrEe</a></p>
<p><a href="/pages/four/" data-order="3.33333">fOuR</a></p>
<p><a href="/pages/five/" data-order="4">FiVe</a></p>
<p><a href="/pages/eleven/" data-order="11">ElEvEn</a></p>

And if I change it to sort by data.title instead:

{% for p in collections.nav | sort(false, false, 'data.title') %}

OUTPUT

<p><a href="/pages/eleven/" data-order="11">ElEvEn</a></p>
<p><a href="/pages/five/" data-order="4">FiVe</a></p>
<p><a href="/pages/four/" data-order="3.33333">fOuR</a></p>
<p><a href="/pages/one/" data-order="1">OnE</a></p>
<p><a href="/pages/three/" data-order="3">tHrEe</a></p>
<p><a href="/pages/two/" data-order="2">TwO</a></p>
<p><a href="/pages/zero/" data-order="0">ZeRo</a></p>

UPDATE Also reminded me of mozilla/nunjucks#1302. I think earlier versions of Nunjucks didn't support sorting by nested props. I think this was added in v3.2.3, per https://github.com/mozilla/nunjucks/blob/master/CHANGELOG.md#323-feb-15-2021.

@kalpeshsingh
Copy link

Does the nunjucks sort filter (standard to the language) actually work? I cannot get it to work using the syntax
{% for page in collections.nav | sort(false, false, 'order') %}

I got this working, but got some unexpected results if I didn't have an order property defined in one of my templates. 🤷

{% for p in collections.nav | sort(false, false, 'data.order') %}
  <p><a href="{{ p.url | url }}" data-order="{{ p.data.order }}">{{ p.data.title }}</a></p>
{% endfor %}

OUTPUT

<p><a href="/pages/zero/" data-order="0">ZeRo</a></p>
<p><a href="/pages/one/" data-order="1">OnE</a></p>
<p><a href="/pages/two/" data-order="2">TwO</a></p>
<p><a href="/pages/three/" data-order="3">tHrEe</a></p>
<p><a href="/pages/four/" data-order="3.33333">fOuR</a></p>
<p><a href="/pages/five/" data-order="4">FiVe</a></p>
<p><a href="/pages/eleven/" data-order="11">ElEvEn</a></p>

And if I change it to sort by data.title instead:

{% for p in collections.nav | sort(false, false, 'data.title') %}

OUTPUT

<p><a href="/pages/eleven/" data-order="11">ElEvEn</a></p>
<p><a href="/pages/five/" data-order="4">FiVe</a></p>
<p><a href="/pages/four/" data-order="3.33333">fOuR</a></p>
<p><a href="/pages/one/" data-order="1">OnE</a></p>
<p><a href="/pages/three/" data-order="3">tHrEe</a></p>
<p><a href="/pages/two/" data-order="2">TwO</a></p>
<p><a href="/pages/zero/" data-order="0">ZeRo</a></p>

UPDATE Also reminded me of mozilla/nunjucks#1302. I think earlier versions of Nunjucks didn't support sorting by nested props. I think this was added in v3.2.3, per https://github.com/mozilla/nunjucks/blob/master/CHANGELOG.md#323-feb-15-2021.

This worked for me.
Thank you.

@palomakop
Copy link

palomakop commented Aug 10, 2024

Since this thread seems to be a helpful source of reference, I'll add the method I used for sorting a collection in a Liquid template. In my case, I was creating a portfolio sorted reverse-chronologically by year (the "workYear" is an integer in the front matter).

{% assign workCollection = collections.work | sort: "data.workYear" | reverse %}

{%- for work in workCollection -%}
    <a href="{{ work.url }}">
    <!-- and the rest of my html etc -->
    </a>
  {%- endfor -%}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests