Open richardherbert opened 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.
@richardherbert Possibly this? https://www.11ty.dev/docs/quicktips/tag-pages/
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?
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.
Yes, I tried dumping {{ tag }}
but there was nothing there - strange??
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.
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.
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.
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.
Excuse me ignorance @tcurdt but I don't understand what youe uniqueTags
filter is returning?
@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>
...
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>
@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.
Simply what I'm trying to do is get an array of content objects grouped by tags that I can iterate over and display.
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" }}
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") %}
Nunjucks has a built-in
sort
filter you could probably use (depending on what thecollections.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.
@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.
@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...
Result!
Any suggestions to tighten this up would be more than welcome!
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.
@ThePeach - Thanks, I'll take a look at that.
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>
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))
});
@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?
@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?
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?
@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));
});
@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.
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)
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));
});
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>
How can I find all the tags used by all my content?