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

Help requested: How to create separate collections using other frontmatter #259

Open
edwardhorsford opened this issue Sep 28, 2018 · 11 comments

Comments

@edwardhorsford
Copy link
Contributor

commented Sep 28, 2018

I'd like to be able to create separate sets of tags - so that I can do two sets of independent tag pages. For example, I plan to have blog posts with tags (and tag pages), and a separate photography section with tags.

I'd also like to use tags for internal IA / logic, but not expose these on tag pages. I know I can filter values, but being able to use other frontmatter properties would be useful.

I think I can achieve this with a custom collection, but it's a little beyond my ability. I wonder if anyone can help?

Something like collections.photoTags that would contain all the tags from the frontmatter photoTags and within them each item with that tag. Recreate what already happens with tags for another field.

My thinking is that I need to loop over all items, look for a designated data (photoTags), then create a new collection for each tag found, containing all items that have that tag. Does that sound correct?

Alternately, is there a simpler way to do what I want?

@edwardhorsford edwardhorsford changed the title How to create separate sets of tags How to create separate collections using other frontmatter Sep 28, 2018

@edwardhorsford edwardhorsford changed the title How to create separate collections using other frontmatter Help requested: How to create separate collections using other frontmatter Sep 28, 2018

@edwardhorsford

This comment has been minimized.

Copy link
Contributor Author

commented Sep 28, 2018

I've found the internal functions that produce the current tagged collections, but it's beyond my abilities to stitch them together. My knowledge of this is lacking especially! If anyone can help I'd be very grateful. Found in this file and this file.

Non working code:

var frontMatterKey = 'photoTags'

getFilteredByTag(tagName) {
    return this.getAllSorted().filter(function(item) {
      let match = false;
      if (!tagName) {
        return true;
      } else if (Array.isArray(item.data.tags)) {
        item.data[frontMatterKey].forEach(tag => {
          if (tag === tagName) {
            match = true;
          }
        });
        // This branch should no longer be necessary per TemplateContent.cleanupFrontMatterData
      } else if (typeof item.data[frontMatterKey] === "string") {
        match = item.data[frontMatterKey] === tagName;
      }
      return match;
    });
  }

getAllTags() {
    let allTags = {};
    for (let map of this.map) {
      let tags = map.data[frontMatterKey];
      if (Array.isArray(tags)) {
        for (let tag of tags) {
          allTags[tag] = true;
        }
        // This branch should no longer be necessary per TemplateContent.cleanupFrontMatterData
      } else if (tags) {
        allTags[tags] = true;
      }
    }
    return Object.keys(allTags);
  }

createTemplateMapCopy(filteredMap) {
    let copy = [];
    for (let map of filteredMap) {
      // let mapCopy = lodashClone(map);

      // TODO try this instead of lodash.clone
      let mapCopy = Object.assign({}, map);

      copy.push(mapCopy);
    }

    return copy;
  }

getTaggedCollectionsData() {
    let collections = {};
    collections.all = this.createTemplateMapCopy(
      this.collection.getAllSorted()
    );
    // debug(`Collection: collections.all size: ${collections.all.length}`);

    let tags = this.getAllTags();
    for (let tag of tags) {
      collections[tag] = this.createTemplateMapCopy(
        this.collection.getFilteredByTag(tag)
      );
      // debug(`Collection: collections.${tag} size: ${collections[tag].length}`);
    }
    return collections;
  }


module.exports = function(collection) {

var newTags = getTaggedCollectionsData();

return newTags;

};
@zachleat

This comment has been minimized.

Copy link
Member

commented Sep 29, 2018

Whoa you're way overcomplicating this. You can make a collection yourself based on any data value or even a file path using the custom collections API.

Look at the second example in this section https://www.11ty.io/docs/collections/#getall()

@edwardhorsford

This comment has been minimized.

Copy link
Contributor Author

commented Sep 29, 2018

Thanks for the pointer - I'm still struggling though.

I'm am trying to create a custom collection (see end module.exports) - since eleventy does what I want internally, I grabbed the existing functions out of there.

I'm not sure getAll does quite what I want - the way I read it is that it lets me get a set of items that share a common thing. If I have an array of photoTags, I'd have to do it for each one. I want to dynamically create sub-collections for each item in photoTags.

Essentially this is recreating the existing functionality applied to tags (which generate collections) but for another item.

I'd like frontmatter like this:

---
photoTags:
 - favourite
 - street
 - candid
---

And then to end up with collections:

collection.photoTags.favourite
collection.photoTags.street
collection.photoTags.candid

Then I would then paginate on colleciton.photoTags and be able to look in collection.photoTags.favourite to get the set of items that have that photoTag.

@edwardhorsford

This comment has been minimized.

Copy link
Contributor Author

commented Oct 4, 2018

@zachleat or others: anyone able to help with this?

I can see how I can make a collection of items based on a key. Eg all items with the key 'photoTag'.

I can also make collections that are derived from a key. Eg a list of all photoTags.

I'm struggling to combine the two.

For each item in the array photoTags, get all the items with that tag.

I've almost got some code working, but I think my object structure is slightly wonky, and it won't render.

@cathydutton

This comment has been minimized.

Copy link

commented Oct 29, 2018

I think I'm having a similar issue, I have two collections Posts and Projects, I'd like to add tags to both which I can do but if I filter the projects collection by a tag I get results form the
post collection included. I'm not sure if what I want to achieve is possible?

@edwardhorsford

This comment has been minimized.

Copy link
Contributor Author

commented Oct 29, 2018

I figured this out in the end and made some generic functions so I can create collections sets based on arbitrary frontmatter. Might be a nice feature to add to Eleventy. Eg it does it for tags by default, but can do it to other keys too on request.


I think you've got a few options:

  • Have both sets of things under tags and when you filter by tag, you also check if it's a post or a project.
  • Create a new frontmatter data key for a new set of tags. Create a new collection set for this new frontmatter.
  • Halfway between the two. Have both use tags, but create two new collection sets that filter out items from the other category. This might be the most flexible.

FWIW I'm starting to think that the existing collections would be better nested under collections.tags rather than just collections. I feel it's at a weird level if you start adding your own collections, or sets of collections based on frontmatter. It just so happens tags are special and get auto-collections - but if you add your own, they have to be at a different level. @zachleat any thoughts?


Below is the code I wrote to create my own 'auto-collections'. As I'm not a developer, I'm guessing it's a bit awkward in places. In particular I'd rather not return it wrapped in an array (which requires me to access the data at collections.foo[0]) - but because of #277 I couldn't get that to work. This is a cut down version from what I'm using so might have a typo from me modifying it to post.

const resolvePath = require('object-resolve-path'); 

const sortAphabetical = (x, y) => {
  if(x.toLowerCase() !== y.toLowerCase()) {
    x = x.toLowerCase();
    y = y.toLowerCase();
  }
  return x > y ? 1 : (x < y ? -1 : 0);
}

const getKeys = (dataSource, key) => {
  let keySet = new Set();

  dataSource.forEach( item => {

    if (!item.hidden){
      var keys = resolvePath(item, String(key));
      if( keys ) {
        if( typeof keys === "string" ) {
          keys = [keys];
        }
        if( typeof keys === "number" ) {
          keys = [String(keys)];
        }
        for (const value of keys) {
          if (!value.startsWith("_")){
            keySet.add(value);
          }
        }
      }
    }
  });
  return [...keySet].sort(sortAphabetical);
}

const getItemsByKey = (dataSource, key, value) => {

  var result = dataSource.filter( item => {
    if (item.hidden){
      return false;
    }
    var keys = resolvePath(item, String(key));
    if(keys) {
      var match = false;
      if( typeof keys === "string" ) {
        keys = [keys];
      }
      if( typeof keys === "number" ) {
        keys = [String(keys)];
      }
      
      keys.forEach( keyValue => {
        if (keyValue == value){
          match = true;
        };
      });
      return match;
    }  
    return false;
  });

  result = result.sort( (a, b) => {
    return b.date - a.date;
  });

  return result;
}

const itemsByKeys = (collection, key) => {
  var keySet = {};
  var newSet = new Set();
  var dataSource = collection.getAll();
    // Collections store useful data in collections.data
  key = 'data.' + String(key);

  keySet = getKeys(dataSource, key);

  keySet.forEach( value => {
    newSet[value] = getItemsByKey(dataSource, key, value);
  });

  return [{...newSet}];
}

// -----------


exports.byTrip = collection => {
  return itemsByKeys(collection, "trip");
}

exports.bySeries = collection => {
  return itemsByKeys(collection, "series");
}

Some other things I do: I ignore keys beginning with underscore. I ignore content with hidden=true.

I then add these as collections with:

  eleventyConfig.addCollection("contentByTrip", require("./utils/collections/contentByKey").byTrip);
  eleventyConfig.addCollection("contentBySeries", require("./utils/collections/contentByKey").bySeries);

I access the data like this:

<ul class='horizontal-list'>
{% for tag, tagItems in collections.contentByTrip[0] %}
  {% set tagUrl %}{{slug}}{{ tag | slugify }}/{% endset %}
  <li><a href="{{ tagUrl }}" class="tag">{{ tag }}</a></li>
{% endfor %}
</ul>

Edit I should also say that I'm using object-resolve-path because whilst Eleventy stores frontmatter data in data.foo, my other data sources don't. This lets me make the functions more generic so I can get blobs of content by a key, regardless of the nesting of that key.

For example, I'm creating collection sets based on exif attributes in my images - where the exif data is stored in a very nested way.

@edwardhorsford

This comment has been minimized.

Copy link
Contributor Author

commented Oct 29, 2018

Also, hi @cathydutton! 👋

@cathydutton

This comment has been minimized.

Copy link

commented Oct 30, 2018

Hi @edwardhorsford Thanks for your help, I've gone with a new frontmatter data key for the projects collection using the code here https://www.11ty.io/docs/collections/#getall()

It creates the filtered projectTags folders as expected, I just cant work out how to display the posts now

@zachleat

This comment has been minimized.

Copy link
Member

commented Jan 14, 2019

Just as a note for me, I did implement something kinda like this for the “Notes” section on my website here https://github.com/zachleat/zachleat.com/tree/master/web/notes

It’s managed by a parent collection that holds all the notes under the note tag. And then I subdivide things using note-tags here: https://github.com/zachleat/zachleat.com/blob/master/web/notes/get-all-font-sizes.md

@zachleat

This comment has been minimized.

Copy link
Member

commented Jan 14, 2019

See also noteTagList and noteTagCollections here: https://github.com/zachleat/zachleat.com/blob/master/web/.eleventy.js#L139

@nhoizey

This comment has been minimized.

Copy link
Contributor

commented Aug 27, 2019

Coming here from https://fuzzylogic.me/thoughts/flexible-tag-style-collections-and-pages-for-non-tag-key-in-eleventy/

It looks like providing a generic way to create as much collections as we want, in addition to tags, could be useful quite often.

I would need this in multiple projects too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.