11ty / eleventy

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

Collection of Tags #927

Open richardherbert opened 4 years ago

richardherbert commented 4 years ago

How can I find all the tags used by all my content?

richardherbert commented 4 years ago

I'm looking to get an array of tags that I can then iterate over and then use getFilteredByTag() to get the content of the tag. Like grouping my content by tag.

pdehaan commented 4 years ago

@richardherbert Possibly this? https://www.11ty.dev/docs/quicktips/tag-pages/

richardherbert commented 4 years ago

Thanks @pdehaan I hadn't found that page. On the surface using collections[ tags ] looked like a good find but I'm using Liquid for my templating and all I get is an empty array...

<ul>
    {% assign taglist = collections[ tag ] %}
    {%- for tag in taglist -%}
        <li>tags: {{ tag.data.title }}</li>
    {%- endfor -%}
</ul>
tags:
tags:
tags:
tags:
tags:
tags:
tags:
tags:

In fact using {% assign taglist = collections %} gives the same result.

I don't seem to be able to access the objects in the array?

pdehaan commented 4 years ago

Sorry, I don't know Liquid (I'm generally a Nunjucks guy), but it looks like you're finding 8 tags/collections, based on your loop output above. I'd start by dumping the {{ tag.data }} or {{ tag }} and see if you can see any output. It's possible that Liquid just isn't finding tag.data.title for some reason.

richardherbert commented 4 years ago

Yes, I tried dumping {{ tag }} but there was nothing there - strange??

pdehaan commented 4 years ago

Yes, I tried dumping {{ tag }} but there was nothing there - strange??

A little strange. I'm not sure why it's looping if it isn't finding anything. If you have the code in GitHub or similar, I can try pulling the repo and taking a quick look.

pdehaan commented 4 years ago

I did get a bit closer on this (using Nunjucks still though, haven't tried w/ Liquid).

But here's my Nunjucks file:

{# eleventyConfig.addFilter("keys", obj => Object.keys(obj)); #}
<p>{{ collections | keys | dump(2) | safe }}</p>

<section>
  {%- for tag, posts in collections %}
  tag: {{ tag }}, posts: {{ posts | length }}<br/>
  {%- endfor %}
</section>

<hr>

<section>
  {%- for tag2, posts2 in collections | dictsort %}
  tag: {{ tag2 }}, posts: {{ posts2 | length }};<br/>
  {%- endfor %}
</section>

Where the custom keys filter is just this:

eleventyConfig.addFilter("keys", obj => Object.keys(obj));

And the output is:

<p>[ 'all', 'page', 'contact', 'post', 'about' ]</p>

<section>
  tag: all, posts: 7<br/>
  tag: page, posts: 2<br/>
  tag: contact, posts: 1<br/>
  tag: post, posts: 2<br/>
  tag: about, posts: 1<br/>
</section>

<hr>

<section>
  tag: about, posts: 1;<br/>
  tag: all, posts: 7;<br/>
  tag: contact, posts: 1;<br/>
  tag: page, posts: 2;<br/>
  tag: post, posts: 2;<br/>
</section>

The one interesting thing about the output, is that it seems like the tag order isn't guaranteed. Each time I regenerated the site, the order of the keys was slightly different. So if you want a specific sorting (ie: sort by number of posts descending), you might need to get a bit creative. Sorting alphabetically is probably trivial since you could probably just do:

<p>{{ collections | keys | sort }}</p>

I can also try cleaning up my repo and push it to GitHub if that'd help. I started with that, and then for some reason got distracted and started porting all the built-in Liquid filters so they'd work with Nunjucks.

richardherbert commented 4 years ago

That's a good lead, thanks. I'll try and translate that into Liquid and knock up a sample repo.

I did think I'd have to drop into JavaScript, not one of my core skills, to write a filter but I was hoping that the issue was me not being about to find the right page in the documentation, being new to this project.

Like you, I'm spinning several project plates atm so I will get back to you later.

tcurdt commented 4 years ago

This issue provides some interesting input. I am looking for a similar thing.

{%- for tag, posts in collections.tech | group %}
<h2>{{tag}}</h2>
{%- for post in posts %}
{{ post.url }}
{%- endfor %}
{%- endfor %}

or

{%- for tag in collections.tech | uniqueTags %}
<h2>{{ tag }}</h2>
{%- for post in collections.tech | selectTag(tag) %}
{{ post.url }}
{%- endfor %}
{%- endfor %}

What I came up with:

  eleventyConfig.addFilter("uniqueTags", (array) => {
    let tags = new Set()
    for (const p of array) {
      if (p.data && p.data.tags) {
        for (const tag of p.data.tags) {
          tags.add(tag)
        }
      }
    }
    return [...tags]
  })
  eleventyConfig.addFilter("selectTag", (array, tag) => {
    return array.filter((p) => {
      return p.data && p.data.tags && p.data.tags.includes(tag)
    })
  })

Probably not the fastest approach but reasonable flexible.

richardherbert commented 4 years ago

Excuse me ignorance @tcurdt but I don't understand what youe uniqueTags filter is returning?

tcurdt commented 4 years ago

@richardherbert all the tags of the pages passed into the filter. I use the same pattern for my post archive. Maybe that helps to make it clear:

{%- for year in collections.tech | uniqueYears | reverse %}
<h2>{{ year }}</h2>
...
dixonge commented 4 years ago

I'm currently using this code, and am wondering how to sort it alphabetically. Haven't yet found anything in nunjucks documentation:

<ol>
{% for tag in collections.tagList %}
<li>  {% set tagUrl %}/tags/{{ tag }}/{% endset %}
  <a href="{{ tagUrl | url }}" class="tag">{{ tag | title }}</a>
  </li>
{% endfor %}
</ol>
richardherbert commented 4 years ago

@dixonge I'm trying to do this with Liquid, which I didn't make clear at the start, and there doesn't seem to be anything like collections.tagList to get me an array of tags. @pdehaan has a good snippet eleventyConfig.addFilter("keys", obj => Object.keys(obj)); which gets an array of tags but unfortunately also include the all tag which I'm struggling to filter out.

richardherbert commented 4 years ago

Simply what I'm trying to do is get an array of content objects grouped by tags that I can iterate over and display.

pdehaan commented 4 years ago

which gets an array of tags but unfortunately also include the all tag which I'm struggling to filter out.

You could create another custom filter which removes unwanted items from an array. I randomly chose to convert to a Set and then back to an Array, but you could probably do a bit of extra work and leave it as an array.

eleventyConfig.addFilter("keys", obj => Object.keys(obj));
eleventyConfig.addFilter("except", (arr=[], ...values) => {
  const data = new Set(arr);
  for (const item of values) {
    data.delete(item);
  }
  return [...data].sort();
});

And if you're using liquid, you could use the filters like this:

{{ collections | keys | except "all", "Home", "About", "Legal" }}
pdehaan commented 4 years ago

I'm currently using this code, and am wondering how to sort it alphabetically. Haven't yet found anything in nunjucks documentation:

@dixonge: Nunjucks has a built-in sort filter you could probably use (depending on what the collections.tagList looks like, ie: array of strings, or something else):

https://mozilla.github.io/nunjucks/templating.html#sort-arr-reverse-casesens-attr

sort(arr=[], reverse=false, caseSens=false, attr=undefined)
<ol>
{% for tag in collections.tagList | sort %}
<li>  {% set tagUrl %}/tags/{{ tag }}/{% endset %}
  <a href="{{ tagUrl | url }}" class="tag">{{ tag | title }}</a>
  </li>
{% endfor %}
</ol>

If tagList is an array of objects, you should be able to pass a value to "attr" for the key you want to sort by. If the key is nested, I think you're out of luck and would need to write your own custom sort filter/function since I think Nunjucks will only look 1 level deep, last I looked.

{% for tag in collections.tagList | sort(false, false, "objectPropToSortBy") %}
dixonge commented 4 years ago

Nunjucks has a built-in sort filter you could probably use (depending on what the collections.tagList looks like, ie: array of strings, or something else):

Wow, that was it! Thank you! I had seen a reference to that, but no good examples for usage I could wrap my brain around.

pdehaan commented 4 years ago

@dixonge Yeah, I had to dig into the Nunjucks source to try and figure it out. I have a few examples of strings-vs-numbers at https://github.com/pdehaan/11ty-nunjucks-sort-test but I'm realizing it isn't super useful when browsing through GitHub and it's a bit of a pain to clone the repo and build locally to see all the generated output.

richardherbert commented 4 years ago

@pdehaan @dixonge Thank you for your thoughts. So after much thrashing around, Googling, trial and error I think I have a Liquid working solution!

In my .eleventy.js...

eleventyConfig.addFilter( 'keys', obj => Object.keys( obj ) );

eleventyConfig.addFilter( 'except', ( arr=[] ) => {
    return arr.filter( function( value ) {
        return value != 'all';
    } ).sort();
} );

...then in my md page...

{% assign tags = collections | keys | except 'all' %}

<ul>
    {%- for tag in tags -%}
        <li>tag: {{ tag }}</li>
        <ol>
            {%- for tag in collections[tag] -%}
                <li>title: {{ tag.data.title }}</li>
            {%- endfor -%}
        </ol>
    {%- endfor -%}
</ul>

...gives me...

image

Result!

Any suggestions to tighten this up would be more than welcome!

ThePeach commented 4 years ago

sorry if I'm interrupting, but I've noticed the eleventy-base-blog achieves something similar by creating a custom collection: https://github.com/11ty/eleventy-base-blog/blob/master/_11ty/getTagList.js

Dunno if that was what you were looking for. Once you have the tag you can access the collection for that tag and iterate over the items and extract whatever you need from there. I guess.

richardherbert commented 4 years ago

@ThePeach - Thanks, I'll take a look at that.

joaomelo commented 4 years ago

just referencing @ThePeach suggestion with little improving (less code and sorting) you could include the code bellow in your eleventy config

  eleventyConfig.addCollection("tagList", collection => {
    const tagsSet = new Set();
    collection.getAll().forEach(item => {
      if (!item.data.tags) return;
      item.data.tags
        .filter(tag => !['post', 'all'].includes(tag))
        .forEach(tag => tagsSet.add(tag));
    });
    return Array.from(tagsSet).sort();
  });

and access it in your templates:

<section>
  tags list 
  {% for tag in collections.tagList %}
    {{ tag }}
  {% endfor %}
</section>
TrentonAdams commented 3 years ago

Slight refinement over @joaomelo's contribution. When doing list data processing, I like to reduce the operations to a series of functional programming calls if I can.

  eleventyConfig.addCollection('tagList', collections => {
    const tags = collections
      .getAll()
      .reduce((tags, item) => tags.concat(item.data.tags), [])
      .filter(tag => !!tag)
      .filter(tag => tag !== 'post')
      .sort();
    return Array.from(new Set(tags))
  });
nigelwhite commented 3 years ago

@TrentonAdams that's working for me, thanks. Now I need to refine it so the tagList collection only lists tags found within collections.page and not from collections.all. I've tried various things but I can't get it to work. Please could you suggest a way?

TrentonAdams commented 3 years ago

@nigelwhite I'm not at my other computer at the moment, but couldn't you just use collections.page instead of collections.all, and that would ensure what you speak of?

nigelwhite commented 3 years ago

Thanks @TrentonAdams. Yes, that's what I thought too. So, in eleventy.js, I tried

eleventyConfig.addCollection('tagMenu', (collections) => {
        const tags = collections
            .getAll(collections.page)
            .reduce((tags, item) => tags.concat(item.data.tags), [])
            .filter((tag) => !!tag)
            .filter((tag) => tag !== 'page')
            .sort();
        return Array.from(new Set(tags));
    });

but the resulting tag list still includes tags found only in my other content type which is collections.post. Am I maybe putting collections.page in the wrong place in your code?

pdehaan commented 3 years ago

@nigelwhite I don't think that's correct. As far as I can tell, getAll() doesn't take any parameters.

https://www.11ty.dev/docs/collections/#getall() https://github.com/11ty/eleventy/blob/094c9851d98d77fdd6723673c71cff32de9a7f0a/src/TemplateCollection.js#L14-L16


UPDATE: Possibly something like this:

  eleventyConfig.addCollection("pageTags", (collections) => {
    const uniqueTags = collections
      .getFilteredByTag("page")
      .reduce((tags, item) => tags.concat(...item.data.tags), [])
      // Tags to ignore...
      .filter(tag => !!tag && !["page", "post"].includes(tag))
      .sort();
    // Dedupe tags
    return Array.from(new Set(uniqueTags));
  });
TrentonAdams commented 3 years ago

@nigelwhite I was thinking more along the lines of replacing collections.getAll() with collections.page; i.e. only replace the one thing. I'm assuming there is a collections.page, but I haven't looked, and I'm new to eleventy so I wouldn't know. I'm at work again, so don't have access to my code at the moment to try it.

TrentonAdams commented 3 years ago

Okay, I was finally able to take a look. collections.page doesn't exist, so I'm not sure what you're referring to @nigelwhite. Did you create a collection of your own called "page"? If so, only you would know how to do what you ask. If the "page" collections is a simple array of tags then you should just be able to add another filter that checks if the "page" collection "includes" said tag.

If "page" is a tag you've been using, then I'd just add the line that pdehaan mentioned...

.filter(tag => !!tag && !["page", "post"].includes(tag)
nigelwhite commented 3 years ago

Thanks @TrentonAdams and @pdehaan. Yes 'page' and 'post' are two tags that I've been adding in my markdown files. I wanted to use these as an 11ty-style way of having 2 content types on my site. So every .md file has either the 'page' or the 'post' tag - never both. They are also stored in differnt folders, but it seems collections ignores folders so that doesn't help.

Your solution worked, thanks. This is the code that works

eleventyConfig.addCollection('pageTags', (collections) => {
        const uniqueTags = collections
            .getFilteredByTag('page')
            .reduce((tags, item) => tags.concat(item.data.tags), [])
            .filter((tag) => !!tag)
            .filter((tag) => !!tag && !['page', 'post'].includes(tag))
            .sort();
        return Array.from(new Set(uniqueTags));
    });
sombriks commented 1 year ago

if in need of taglist + count posts this can be done (based on everyone else answer of course!

    eleventyConfig
        .addFilter('postTags', tags => Object.keys(tags)
            .filter(k => k !== "posts")
            .filter(k => k !== "all")
            .map(k => ({name: k, count: tags[k].length}))
            .sort((a,b) => b.count - a.count));

enables something like this on templates:

<div style="display: flex; flex-wrap: wrap;">
  {% assign tagList = collections | postTags %}
  {%- for tag in tagList -%}
    <a href="/tag/{{tag.name}}">{{tag.name}}({{tag.count}})</a>
  {%- endfor -%}
</div>