Jekyll exclude tag from pagination - jekyll

I have some posts tagged with foo, and I want to exclude those from my front page.
I have tried putting this code into the front-page template:
<div class="posts">
{% for post in paginator.posts %}
{% unless post.tags and post.tags contains "foo" %}
{% endunless %}
{% endfor %}
</div>
However this results in the pagination being incorrect.
Here are some example posts:
+-------+--------+-----+
| Index | Post | Tag |
+-------+--------+-----+
| 1 | Red | foo |
| 2 | Blue | |
| 3 | White | |
| 4 | Pink | foo |
| 5 | Orange | |
| 6 | Yellow | foo |
| 7 | Beige | foo |
| 8 | Purple | |
| 9 | Black | foo |
+-------+--------+-----+
Actual output:
Page 1: 2, 3, 5
Page 2: 8
What I would like:
Page 1: 2, 3, 5, 8
As you can see it is currently splitting the posts into blocks of 5 and then my code filters them - I would like to apply the filtering before the pagination is calculated.

Just add hidden: true variable in yaml frontmatter for posts not to appear in home page pagination. See this.
While this doesn't filter by tags (you'd need to go through posts one by one and add the option manually) it seems much simpler than the tweaks suggested above.
And pagination for your first and second pages remains correct.

It can't be done without hacking the paginator plugin, so here we go:
remove gem jekyll-paginate from Gemfile
set the needed configuration variables in _config.yml:
paginate: 2
paginate_path: "/blog/page:num/"
create the _plugins directory
copy pager.rb and pagination.rb to _plugins/
cd _plugins
wget https://github.com/jekyll/jekyll-paginate/blob/master/lib/jekyll-paginate/pager.rb
wget https://github.com/jekyll/jekyll-paginate/blob/master/lib/jekyll-paginate/pagination.rb
display posts in homepage with the suggested code used in docs
<!-- This loops through the paginated posts -->
{% for post in paginator.posts %}
<h1>{{ post.title }}</h1>
<p class="author">
<span class="date">{{ post.date }}</span>
</p>
<div class="content">
{{ post.content }}
</div>
{% endfor %}
<h1> Paginator</h1>
<!-- Pagination links -->
<div class="pagination">
{% if paginator.previous_page %}
Previous
{% else %}
<span class="previous">Previous</span>
{% endif %}
<span class="page_number ">Page: {{ paginator.page }} of {{ paginator.total_pages }}</span>
{% if paginator.next_page %}
Next
{% else %}
<span class="next ">Next</span>
{% endif %}
</div>
In pagination.rb modify the paginate function to filter your posts containing the tag foo, currently the all_posts variable contains all the posts data used to calculate pagination, so we need to remove the ones that contains the tag with:
all_posts = all_posts.select do |elem|
!elem.data['tags'].include? 'foo'
end
Then the function will look like:
def paginate(site, page)
all_posts = site.site_payload['site']['posts'].reject { |post| post['hidden'] }
all_posts = all_posts.select do |elem|
!elem.data['tags'].include? 'foo'
end
pages = Pager.calculate_pages(all_posts, site.config['paginate'].to_i)
(1..pages).each do |num_page|
pager = Pager.new(site, num_page, all_posts, pages)
if num_page > 1
newpage = Page.new(site, site.source, page.dir, page.name)
newpage.pager = pager
newpage.dir = Pager.paginate_path(site, num_page)
site.pages << newpage
else
page.pager = pager
end
end
end
Then page 1 will show the desired posts, only the ones not containing the foo tag: 2, 3, 5, 8.

[updated answer]
Solution 1. New collection
You reported that the where command on the paginator object does not work/recalculate the paging. Therefore, I would advice to save all 'foo' posts in a separate collection. This solves your original problem, but creates a new problem if you want to show all posts combined on another page. If you do not want that, this is the most elegant solution.
Solution 2. Use Javascript for paging
Forget about the paginator object and solve this by building you own paging (or infinte scroll) in Javascript, since pagination is a SEO issue anyway...
Note: I have created a resource for Jekyll without plugins. I will add Javascript pagination here too.

Related

Jekyll Liquid Syntax, how to dereference a property

I am grouping the posts on my site using this liquid tag.
{% assign postsForYear = site.posts | group_by_exp:"post", "post.date | date: '%Y'" | where: "name", "2020" %}
When I echo the postForYear out to screen, I see this:
{"name"=>"2020", "items"=>[#, #, #, #, #, #], "size"=>6} 0
Which makes sense, as I have six posts for that year. However, I am trying to dereference that object and get the .size property, which I can see in the output...I can't figure out the syntax!
How to get the .Size property?
None of these work.
{{ postsForYear.size }}
{{ postsForYear.items | size }}
After that, I would love to learn how to foreach my way through the posts...this also seems simple but doesn't work!
Ah, I figured it out. Strangely, the postsForYear array was actually treated like an array itself, so I had to index into the first position to get to the properties.
{% assign postsForYear = site.posts |
group_by_exp:"post", "post.date | date: '%Y'" | where: "name", "2020" %}
###doesn't work
{{ postsForYear.items | size }}
### does work
{{ postsForYear[0].items | size }}
### example
Posts for year {{ postsForYear[0].name }}, total posts {{ postsForYear[0].size }}
>Posts for year 2020, total posts 6
And to answer my other question, how to foreach your way through:
{% for post in postsForYear[0].items %}
<h1>{{post.title}}</h1><br>
{% endfor %}

jinja2 how to retrieve the value of a variable to use it in an if statement jinja2

In order to create a template using jinja and markdown through mkdocs. I met this problem that is blocking me so far.
I am using a variable in my if statement in order to fill a table with some specific information if the condition is true
Let's say that I have different environments (env1 env2 env3) for env1 and env2 the filling of the table doesn't require the if statement to be true but when it comes to env3 the condition must be true so the filling of the table could occur.
My template looks like this
#{{env}}
##servers
{% if {{env}} = 'env3' %}
| | IP | FQDN |
|-------------|----|------|
| | | |
{% for node in server.nodes %}|{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %} {% for node in server.class.nodes %}|classe{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
{% else %}
| | IP | FQDN |
|-------------|----|------|
| servertype1 | {{server.servertype1.ip}} | |
| servertype2 | {{server.servertype2.vip}} | |
{% for node in server.specific.nodes %}|specific{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %} {% for node in server.class.nodes %}|classe{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
{% endif %}
To resume I am having a hard time to retrieve the value of {{env}} (the one in the title since mkdocs generates it correctly) in order to use it in the if statement so when it comes to the specific page of env3 the table with appropriate info is shown.
Here's the error I get :
jinja2.exceptions.TemplateSyntaxError: expected token ':', got '}'
I tried many different variations of the syntax, but the result remains the same.
I am welcoming all the leads or hints in order to solve this issue.
Well I found it !
I just had to adjust my if statement syntax instead of
{% if {{env}} = 'env3' %}
i had to put like this :
{% if env == "env3" %}
there was a missing equal sign and quotes instead of ' ' plus the brackets in env i had to delete them

Variable usage in jinja2 inside markdown template

I am trying to create a template of a markdown file using jinja and the values of the variables are stocked in .yml file ( kind of an inventory of hosts).
my problem is that I think that markdown table that I am trying to fill are not making it easy and since I have tried many alternatives using jinja2 tools and function and still no success I am adressing this issue to the community in hope of getting some insight or tips to get over the problem:
my markdown file contains table such as :
## Servers
### Cluster 1
| | IP | FQDN |
|-------|----|------|
| | | |
my value file .yml is as follows :
servers:
clusters:
- id: 1
test: X.X.X.X
nodes:
- X.X.X.X
- X.X.X.X
- X.X.X.X
in order to retrieve the right values to fill the table I wrote this :
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
| test | {{servers.clusters.id.test}} | |
{% for node in servers.clusters.id.nodes %}|node{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
but it doesn't seem to work and the error is not very explicit (to a jinja2 beginner of course) :
File "[PATH]/filename.md", line 34, in top-level template code
| test | {{server.clusters.id.test}} | |
File "/usr/lib/python3.8/site-packages/jinja2/environment.py", line 471, in getattr
return getattr(obj, attribute)
jinja2.exceptions.UndefinedError: 'list object' has no attribute 'id'
All suggestions are welcome.
You need to loop through the items contained in clusters or reference it by index as it is a list.
The trick is to understand the structure of the data returned by the YAML parser and how to access that structure from within Jinja.
Jinja expressions are mostly just Python code with a few minor differences. For example, Jinja provides a shortcut which allows you to access dicts using dot syntax. Normally, in Python one would do mydict['keyname'] to retrieve a value. However, Jinja also supports doing mydict.keyname Under the hood it actually calls mydict.keyname but when that fails, it tries mydict['keyname'] as a fallback. If both fail, it raises the first error, which is what you are seeing ('list object' has no attribute 'id').
Note that the clusters item contains a list (as indicated by the -), which is why the error refers to a 'list object'. You cannot access items of a list using either mylist.keyname or mylist['keyname']. You either need to loop through the list or reference a specific item by its index by number (mylist[0] for the first item). Unless you are certain that the list will never contain more than one item (in which case why is it a list?) then you will likely want to loop through the items.
It appears that you are trying to only include the data for the single item with id is 1. If that will always be the first item in the list, you could do: servers.clusters.[0].test. However, if you can't be sure of that, then you would need to loop through all items and wrap the actual statement within an if statement which checks that the id is equal to the previous set variable id.
Like this for the IP column of the first row:
{% for cluster in servers.clusters %}{% if cluster.id == id %}{{ cluster.test }}{% endif %}{% endfor %}
Note that cluster.id makes reference to the key id of an item in clusters. It is not the id set by {% set id = 1 %}. However, it can be compared to it and if the two are equal (both contain the value 1 in this case), then the expression is True and the expression contained within is executed. When the two are not equal, then it is ignored.
You have the same issue for the second row. However, nodes also contains a list of strings. There are no attributes on any of those items so you would just render node in the second loop (rather than node.id, node.ip and node.fqdn). Therefore, I assume you want something like this:
{% for cluster in servers.clusters %}{% if cluster.id == id %}|{% for node in cluster.nodes %} {{ node }} |{% endfor %}{% endif %}{% endfor %}
Of course, you could combine that all together and only do the loop once:
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
Naturally, if you wanted to combine all clusters in a single table, you would remove the if check and have a single row for each cluster. But that would be a different table that the one you have asked for.
If you wanted a separate table for each cluster, you have a few options. You could use the same approach and simply redefine the id variable. Like this:
### Cluster 1
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
### Cluster 2
{% set id = 2 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
Note that the only difference between the two are the first two lines:
### Cluster 2
{% set id = 2 %}
Everything else is the same. But if you are just repeating the same code, that is not very efficient. Any future changes would need to be made for each cluster. Instead, just wrap the whole thing up in a loop:
{% for cluster in servers.clusters %}
### Cluster {{ cluster.id }}
| | IP | FQDN |
|-------|----|------|
| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endfor %}
Notice that the loop wraps everything, including the header. The header then gets the cluster.id. And, as the body of the table will be repeated for each cluster, we do not need the if statement confining it to only one cluster.
It is important to note that this approach will only work if the data for each and every cluster is in the same format/structure.

How to group Jekyll pages by the first letter of the title?

I'm building a Jekyll site that has a page for each topic. I want to create an alphabetically grouped list of links to each page.
For example, if my topic titles are:
Aardvark
Beatle
Catfish
Cattle
I want to end up with a list of links organised as:
A
Aardvark
B
Beatle
C
Catfish
Cattle
So far, I have gotten code that looks like this:
{% assign topics_by_letter =
site.topics | group_by_expr: "topic", "topic.title | slice: 0, 1" %}
{% for letter in topics_by_letter %}
<div>
{{ letter.name }}
</div>
{% endfor %}
site.topics is the correct name of the page collection and evaluates as expected.
All topics have a valid title label.
Where I am stuck is that letter.name evaluates to empty and I just have a list of empty divs. The snippet {{ page.title | slice: 0, 1 }} works and gives you back the first letter of the topic title.
What am I missing?
Was a typo, group_by_expr should be group_by_exp.

How to pass variables from layout to page in Liquid?

I try to create a global variable in layout and call it in pages
E.g:
<!-- _layouts/post.html -->
{% assign filename = page.url | split: '/' | last | replace: '.html', '' %}
{{ content }}
in post page:
<!-- SomePage.html -->
<h1>Page Name: {{ filename }}</h1> //-> # Page Name: SomePage
The page name should be printed but I do not know how to do it
Any ideas?
I guess you are misusing Jekyll as it is not suppose to work like that. It should be used other way around. Templates are designed to show pages with variables, so that you follow many to one relation pages in templates.
The idea is to put repetitive part of your pages in your layout. So you should have your layout defined like this:
<!-- _layouts/post.html -->
{% assign filename = page.url | split: '/' | last | replace: '.html', '' %}
<h1>{{ filename }}</h1>
{{ content }}
Then you don't have to write that part in your every page, but it is handled automatically for you.