jekyll assign concat in a loop? - jekyll

I would like to organize a page based on the number of pages that pass a filter.
I have tried to append truthy pages to a collection but it doesn't work.
{% assign homepage_posts = [] %}
{% for my_page in site.pages %}
{% if my_page.homepage %}
{% assign homepage_posts = homepage_posts | concat: [my_page] %}
{% endif %}
{% endfor %}
<h1>size{{homepage_posts.size}}</h1>
<h1>{{homepage_posts}}</h1>
This is not working. Does concat only work with strings?

Jekyll will use Liquid 4 soon. But, for now, no concat.
In your case you can :
Create an empty array (bracket notation doesn't work in liquid) : {% assign homepage_posts = "" | split:"/" %}
{{ homepage_posts | inspect }} --> output : []
And push elements in it :
{% for my_page in site.pages %}
{% if my_page.homepage %}
{% assign homepage_posts = homepage_posts | push: mypage %}
{% endif %}
{% endfor %}
{{ homepage_posts | inspect }}

concat filter only works with arrays and will be available in Jekyll when it upgrades to Liquid 4.*:
concat
Concatenates (combines) an array with another array. The resulting
array contains all the elements of the original arrays. concat will
not remove duplicate entries from the concatenated array unless you
also use the uniq filter.
To filter pages containing a specific attribute (in this case homepage: true) you can use a where filter.
Having a page with front matter:
---
homepage: true
---
Then you can have the pages with the homepage: true attribute like:
{% assign homepages = site.pages | where:"homepage","true" %}

Related

Jekyll: sort collections by size

I want to sort my Jekyll collections by the number of documents that are in each collection.
Each collection in the site.collections variable has a docs field, and the docs field (which is an array of documents) has a size field, which is the number of documents in this collection (see documentation).
However, something like this doesn't work:
{% assign sorted = site.collections | sort: 'docs.size' %}
{% for coll in sorted %}
...
{% endfor %}
It results in a
Liquid Exception: no implicit conversion of String into Integer
It seems that the argument to sort can only be an immediate field of the type of object being sorted, and not a field of a field thereof.
Is there a way to achieve sorting the collections by the number of documents they contain?
Build an array of the available sizes:
{% assign sorted = '' | split: "" %}
{% for coll in site.collections %}
{% assign sorted = sorted| append: coll.docs.size %}
{% endfor %}
Sort the above array.
Iterate the above array and all your collections printing only the collection whose size matches the sorted array number.
Ok, I achieved it in a rather ugly way, along the lines of marcanuy's answer.
<!-- Create a comma-separated string of all the sizes of the collections -->
{% for coll in site.collections %}
{% if coll.title %}
{% if coll.docs.size < 10 %}
{% assign str = coll.docs.size | prepend: "00" %}
{% elsif coll.docs.size < 100 %}
{% assign str = coll.docs.size | prepend: "0" %}
{% else %}
{% assign str = coll.docs.size %}
{% endif %}
{% assign sizes = sizes | append: str | append: "," %}
{% endif %}
{% endfor %}
<!-- Remove last comma of string -->
{% assign length = sizes | size | minus: 1 %}
{% assign sizes = sizes | slice: 0, length %}
<!-- Split string into array, sort DESC, and remove duplicate elements -->
{% assign sizes = sizes | split: "," | sort | reverse | uniq %}
<!-- Iterate through sizes, and for each size print those collections that have this size -->
{% for s in sizes %}
{% for coll in site.collections %}
{% assign i = s | plus: 0 %}
{% if coll.docs.size == i %}
<p>{{ coll.title }}: {{ i }} documents</p>
{% endif %}
{% endfor %}
{% endfor %}
The main difficulty is that an array of sizes created like this, is an array of strings, and sorting it results in an alphabetical sort order, rather than in an numerical one (e.g. "15" comes before "2").
To remedy this, I prepend "00" to numbers less than 10, and "0" to number less than 100. This makes the alphabetical sort order coincide with the desired numerical sort order.
Then I iterate through these sizes (which are still strings), and convert them to integers on the fly (by plus: 0) so that I can compare them to the docs.size field of each collection.
It's pretty verbose, but since this is executed only when the site is generated, and not at each request in production mode, it's ok.
Still, better solutions are welcome!

Liquid filter collection where not null

In my front matter for some pages (not all) I have:
---
top-navigation:
order: 2
---
Using liquid I want to filter all site pages which have a top-navigation object and sort by top-navigation.order.
I'm trying sort:'top-navigation.order' but that's throwing an exception of undefined method [] for nil:NilClass. I tried where:"top-navigation", true but it's not equating truthy values.
How can I filter for pages that have top-navigation and then sort?
Two steps:
Create an array with pages that contains the top-navigation key.
We create an empty array and then push the items that have the key.
{% assign navposts = ''|split:''%}
{% for post in site.posts %}
{% if post.top-navigation %}
{% assign navposts = navposts|push:post%}
{% endif %}
{% endfor %}
Sort the above array by top-navigation.order
{% assign navposts = navposts|sort: "top-navigation.order"%}
Printing results:
{% for post in navposts %}
<br>{{ post.title }} - {{post.top-navigation}}
{% endfor %}
For pages use site.pages.
In Jekyll 3.2.0+ (and Github Pages) you can use the where_exp filter like so:
{% assign posts_with_nav = site.posts | where_exp: "post", "post.top-navigation" %}
Here, for each item in site.posts, we bind it to the 'post' variable and then evaluate the expression 'post.top-navigation'. If it evaluates truthy, then it will be selected.
Then, putting it together with the sorting, you'd have this:
{%
assign sorted_posts_with_nav = site.posts
| where_exp: "post", "post.top-navigation"
| sort: "top-navigation.order"
%}
Liquid also has the where filter which, when you don't give it a target value, selects all elements with a truthy value for that property:
{% assign posts_with_nav = site.posts | where: "top-navigation" %}
Unfortunately, this variant does not work with Jekyll.

Filter Array Liquid in Jekyll

I have an include which renders my posts in a certain format. I am passing the posts to the include like below:
{% include post-list.html posts=site.posts %}
However, I would like to filter out a certain category before passing it to the include. Does anyone have any ideas how I can achieve this?
You can do it defining a variable containing an array with all the posts not containing a specific category and then passing that to includes, for example to filter posts containing the category jekyll:
We create the array {% assign notjekyllposts = "" | split: "" %}
Loop all posts {% for apost in site.posts %}
Filter those that does not contain the jekyll category adding them to the array: {% assign notjekyllposts = notjekyllposts | push: apost %}
Pass the variable to the include tag
Putting it all together:
{% assign notjekyllposts = "" | split: "" %}
{% for apost in site.posts %}
{% unless apost.categories contains 'jekyll' %}
{% assign notjekyllposts = notjekyllposts | push: apost %}
{% endunless %}
{% endfor %}
{% include post-list.html posts=notjekyllposts %}

How to get datas with multiple variables in path with jekyll and liquid

At its most basic level I need to append a partial object path onto an existing object path. In this particular instance I can't use plugins.
Say you have an object path:
{{ site.data.grants.2015.Return.ReturnHeader.ReturnTypeCd }}
Which, of course, can also be referenced as follows:
{% assign var = "ReturnTypeCd" %}
{{ site.data.grants.2015.Return.ReturnHeader[var] }}
How would I go about adding additional levels of nesting to the variable?
{% assign xTest = "Return.ReturnHeader.ReturnTypeCd" %}
{{ site.data.grants.2015[xTest] }}
//does not work
I've played around with both dot and bracket notations and using append as well as capture, but can't seem to find a solution that works.
This works :
Data file is _data/grants.yml
"2015":
Return:
ReturnHeader:
ReturnTypeCd: "Et hop !"
Getting deep target with a "dotted" string :
{% assign dataPath = site.data.grants.2015 %}
{% assign target = "Return.ReturnHeader.ReturnTypeCd" %}
{% comment %} ++++ Transform target string to an array {% endcomment %}
{% assign labels = target | split:"." %}
{% comment %} ++++
Looping in labels array and reassigning dataPath on each loop.
This goes deeper and deeper in the data tree
++++ {% endcomment %}
{% for label in labels %}
<h2>Label : {{ label }}</h2>
{% assign dataPath = dataPath[label] %}
<p>dataPath : {{ dataPath }}</p>
{% endfor %}

Jekyll - Get all posts that are in multiple categories

I want to loop over all posts that are assigned category "foo" and category "bar" ..
{% for post in site.categories.foo and in site.categories.bar %}
Is this possible?
In my case "foo" as a "parent" category to "bar" ... /foo/bar/_posts
Thanks
Instead of looking through every post and matching with an or, you can filter by the first tag and then look for the second (and third, fourth, fifth...) tags:
{% for post in site.categories.fizz%}
{% if post.categories contains "buzz" and post.categories contains "bang" %}
<!--post has categories fizz AND buzz AND bang-->
<li>{{ post.title }}</li>
{% endif %}
{% endfor %}
This is a little more efficient than iterating over every single post, and it sets up an and relationship instead of an or relationship.
It is fully possible: loop over all posts, and then select the wanted posts:
{% for post in site.posts %}
{% if post.categories contains "foo" or post.categories contains "bar" %}
<li>{{ post.title }}</li>
{% endif %}
{% endfor %}
Using the Where Expression filter in Jekyll.
Jekyll Liquid Filter Docs
Where Expression,
Select all the objects in an array where the expression is true.
3.2.0
{{ site.members | where_exp:"item", "item.projects contains 'foo'" }}
So on my site I did:
_includes/clippings.html
...
{% capture _filter %}item.tags contains '{{ include.tag }}'{% endcapture %}
{% for clip in site.clippings | where_exp: 'item', _filter %}
{{ clip.do_stuff }}
# more html and stuff
{ endfor }
{% include clippings.html tag='foo' %}
In this case I need to specify the filter tag dynamically. And clippings is just a collection like posts.
If you want to filter by multiple static tags you could do something like:
{% for post in site.posts | where_exp: 'item', "item.tags contains 'foo'" | where_exp: 'item', "item.tags contains 'bar'" %}
{{ post.do_stuff }}
{ endfor }
If you want to do multiple dynamic filter tags then you will need to do something similar to the capture stuff I did above.
I have not tested this, but it should filter posts by an arbitrary amount of filter tags.
{% assign posts = site.posts %}
{% filter_tags = 'foo, bar, buzz' | slipt: ', ' %}
{% for tag in filter_tags %}
{% capture _filter %}item.tags contains '{{ tag }}'{% endcapture %}
{% assign posts = posts | where_exp: 'items', _filter %}
{% endfor %}
{% for post in posts %}
{{ post.do_stuff }}
{% endfor %}
However looping over the whole thing once and checking each post might be more efficient at that point.