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 · 13 comments
Labels
needs-discussion Please leave your opinion! This request is open for feedback from devs.

Comments

@edwardhorsford
Copy link
Contributor

edwardhorsford 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
Copy link
Contributor Author

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
Copy link
Member

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
Copy link
Contributor Author

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
Copy link
Contributor Author

@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
Copy link

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
Copy link
Contributor Author

edwardhorsford 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
Copy link
Contributor Author

Also, hi @cathydutton! 👋

@cathydutton
Copy link

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
Copy link
Member

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
Copy link
Member

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

@zachleat zachleat added the needs-discussion Please leave your opinion! This request is open for feedback from devs. label Mar 8, 2019
@nhoizey
Copy link
Contributor

nhoizey 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.

@Pete-Robelou
Copy link

I have a similar issue I need to resolve. I need to create paginated tag pages for individual collections without any contamination from other collections.

My site includes blog posts, case studies and press releases. I'm currently using the zero maintenance tag pages but the problem I have is blog posts, case studies and press releases are all appearing on the same tag listing pages if they share a tag.

E.g.

  • blog post (tags: 'star wars', 'space')
  • case study (tags: 'star trek', 'space')

I've looked at the other solutions mentioned in this ticket and while I have a basic understanding of their concepts I'm still really struggling to resolve the issue.

I have created separate collections for blogPosts, blogPostTags, case-studies, caseStudyTags, pressReleases, pressReleaseTags and this allows me to list them all out on their own page while using the xxxTags collections to create a dropdown filter that redirects to one of the tag pages.

I have been looking into the pagination before function to see if I could filter one of the collections before the pagination kicks in but I'm not sure this is the right way to do it. Even if I could filter the blogPosts collection so it only returns posts that have the tag 'space' for example, how would I get it to iterate over all the blogPostTags and create tag pages in the same way the zero maintenance does. I.e every time I add a new tag to a blog post a new tag page is automatically created.

Does it make sense to create a unique taxonomy in the frontmatter for each collection, e.g. blog-post-tags: [ tag, tag] and case-study-tags: [ tag, tag] rather than using tags:? I'm guessing this would prevent any contamination of blog posts, case studies and press releases from being added to the same collection. I'm not sure how I would translate this into zero maintenance tag pages though.

Any advice would be greatly appreciated.

@MattMcAdams
Copy link

MattMcAdams commented Mar 6, 2022

Alright so for me, I needed two isolated collections "posts" and "projects", and it seems that the best way to handle it is to create three collections for each content type:

First, you want a collection that will get all items of a single content type. This is the easiest part and I chose to do this by file structure to avoid relying on functional frontmatter or junk tags.

eleventyConfig.addCollection("posts", function (collectionAPI) {
  return collectionAPI.getFilteredByGlob("./src/posts/*.md");
});

Next, you'll want a list of tags used by each content type. This can be used for Zero Maintenance Tag Pages and creating tag lists.

I made this as a function, because I'll be using it several times

// Return a list of tags in a given collection
function getTagList(collection) {
  let tagSet = new Set();
  collection.forEach((item) => {
    (item.data.tags || []).forEach((tag) => tagSet.add(tag));
  });
  return [...tagSet];
}

Then create the collection with:

eleventyConfig.addCollection("postTags", function (collectionAPI) {
  let collection = collectionAPI.getFilteredByGlob("./src/posts/*.md");
  return getTagList(collection);
});

Lastly you'll want a way to list all of the items in a content type with a given tag. This is a bit more complicated and huge credit to Laurence Hughes who figured out the base of my solution.

// Return an object with arrays of posts by tag from the provided collection
function createCollectionsByTag(collection) {
  // set the result as an object
  let resultArrays = {};
  // loop through each item in the provided collection
  collection.forEach((item) => {
    // loop through the tags of each item
    item.data.tags.forEach((tag) => {
      // If the tag has not already been added to the object, add it as an empty array
      if (!resultArrays[tag]) { resultArrays[tag] = []; }
      // Add the item to the tag's array
      resultArrays[tag].push(item);
    });
  });
  // Return the object containing tags and their arrays of posts
  // { tag-name: [post-object, post-object], tag-name: [post-object, post-object] }
  return resultArrays;
}

Now we can create the actual collection with this:

eleventyConfig.addCollection("postsTagged", function (collectionAPI) {
  let collection = collectionAPI.getFilteredByGlob("./src/posts/*.md");
  return createCollectionsByTag(collection);
});

With all the above added, I can now:

  • Get a list of all posts with collections.posts
  • Get a list of all tags used by posts with collections.postTags
  • Get a list of all posts with a specific tag with collections.postsTagged["tag"]

Hopefully this helps someone out. I also wrote a blog post going over this for reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-discussion Please leave your opinion! This request is open for feedback from devs.
Projects
None yet
Development

No branches or pull requests

6 participants