How to skip the first element in an array? - jekyll

Is it possible in Jekyll to skip the first element of a list. For instance, if my page.url is /blog/1/2/3/4, I'd like to get just 1/2/3/4 by doing something like {{ page.url | split:'/' | skip:'1'| join:'/' }}

Well, I ended up finding my own answer:
page.url | split:'/' | slice:2,10 | join:'/'

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.

Jekyll exclude tag from pagination

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.

Jekyll strip_html but allow certain HTML tags

In my Jekyll post I'm using strip_html to display a brief 50-word intro of the post on the main page:
<p>{{ post.content | strip_html | truncatewords:50 }}</p>
strip_html removes all HTML from this excerpt. But I'd like so include some HTML, specifically <i> and <p>.
Is there a configuration (_config.yaml) for doing that, or is there a limitation where strip_html doesn't have customization?
AFAIK there is no built-in way to customize strip_html.
If you have an exclusive -- and not so long -- list of your wanted tags, you may first use replace on the tags you want to keep to replace them with non html markers, then use strip_html, and replace again to get back the html:
{% assign preprocessed_content=post.content | replace: '<p>', '__p__' %}
{% assign preprocessed_content=preprocessed_content | replace: '</p>', '__/p__' %}
{% assign truncated_content=preprocessed_content | strip_html | truncatewords:50 %}
{% assign cleaned_content=truncated_content | replace: '__p__', '<p>' %}
{% assign cleaned_content=cleaned_content | replace: '__/p__', '</p>' %}
<p>{{ cleaned_content }}</p>