11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
17.1k stars 494 forks source link

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

Closed noelforte closed 4 years ago

noelforte commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 https://github.com/mozilla/nunjucks/pull/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 commented 1 year ago

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 commented 2 months ago

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 -%}