Sorted navigation menu with Jekyll and Liquid - jekyll

I'm constructing a static site (no blog) with Jekyll/Liquid. I want it to have an auto-generated navigation menu that lists all existing pages and highlight the current page. The items should be added to the menu in a particular order. Therefore, I define a weight property in the pages' YAML:
---
layout : default
title : Some title
weight : 5
---
The navigation menu is constructed as follows:
<ul>
{% for p in site.pages | sort:weight %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
{{ p.title }}
</a>
</li>
{% endfor %}
</ul>
This creates links to all existing pages, but they're unsorted, the sort filter seems to be ignored. Obviously, I'm doing something wrong, but I can't figure out what.

Since Jekyll 2.2.0 you can sort an array of objects by any object property. You can now do :
{% assign pages = site.pages | sort:"weight" %}
<ul>
{% for p in pages %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
{{ p.title }}
</a>
</li>
{% endfor %}
</ul>
And save a lot of build time compared to #kikito solution.
edit:
You MUST assign your sorting property as an integer weight: 10 and not as a string weight: "10".
Assigning sorting properties as string will ends up in a a string sort like "1, 10, 11, 2, 20, ..."

Your only option seems to be using a double loop.
<ul>
{% for weight in (1..10) %}
{% for p in site.pages %}
{% if p.weight == weight %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
{{ p.title }}
</a>
</li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
Ugly as it is, it should work. If you also have pages without a weight, you will have to include an additional internal loop just doing {% unless p.weight %} before/after the current internal one.

Below solution works on Github (doesn't require a plugin):
{% assign sorted_pages = site.pages | sort:"name" %}
{% for node in sorted_pages %}
<li>{{node.title}}</li>
{% endfor %}
Above snippet sorts pages by file name (name attribute on Page object is derived from file name). I renamed files to match my desired order: 00-index.md, 01-about.md – and presto! Pages are ordered.
One gotcha is that those number prefixes end up in the URLs, which looks awkward for most pages and is a real problem in with 00-index.html. Permalilnks to the rescue:
---
layout: default
title: News
permalink: "index.html"
---
P.S. I wanted to be clever and add custom attributes just for sorting. Unfortunately custom attributes are not accessible as methods on Page class and thus can't be used for sorting:
{% assign sorted_pages = site.pages | sort:"weight" %} #bummer

I've written a simple Jekyll plugin to solve this issue:
Copy sorted_for.rb from https://gist.github.com/3765912 to _plugins subdirectory of your Jekyll project:
module Jekyll
class SortedForTag < Liquid::For
def render(context)
sorted_collection = context[#collection_name].dup
sorted_collection.sort_by! { |i| i.to_liquid[#attributes['sort_by']] }
sorted_collection_name = "#{#collection_name}_sorted".sub('.', '_')
context[sorted_collection_name] = sorted_collection
#collection_name = sorted_collection_name
super
end
def end_tag
'endsorted_for'
end
end
end
Liquid::Template.register_tag('sorted_for', Jekyll::SortedForTag)
Use tag sorted_for instead of for with sort_by:property parameter to sort by given property. You can also add reversed just like the original for.
Don't forget to use different end tag endsorted_for.
In your case the usage look like this:
<ul>
{% sorted_for p in site.pages sort_by:weight %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
{{ p.title }}
</a>
</li>
{% endsorted_for %}
</ul>

The simplest solution would be to prefix the filename of your pages with an index like this:
00-home.html
01-services.html
02-page3.html
Pages are be ordered by filename. However, now you'll have ugly urls.
In your yaml front matter sections you can override the generated url by setting the permalink variable.
For instance:
---
layout: default
permalink: index.html
---

Easy solution:
Assign a sorted array of site.pages first then run a for loop on the array.
Your code will look like:
{% assign links = site.pages | sort: 'weight' %}
{% for p in links %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">
{{ p.title }}
</a>
</li>
{% endfor %}
This works in my navbar _include which is simply:
<section id="navbar">
<nav>
{% assign tabs = site.pages | sort: 'weight' %}
{% for p in tabs %}
<span class="navitem">{{ p.title }}</span>
{% endfor %}
</nav>
</section>

I've solved this using a generator. The generator iterates over pages, getting the navigation data, sorting it and pushing it back to the site config. From there Liquid can retrieve the data and display it. It also takes care of hiding and showing items.
Consider this page fragment:
---
navigation:
title: Page name
weight: 100
show: true
---
content.
The navigation is rendered with this Liquid fragment:
{% for p in site.navigation %}
<li>
<a {% if p.url == page.url %}class="active"{% endif %} href="{{ p.url }}">{{ p.navigation.title }}</a>
</li>
{% endfor %}
Put the following code in a file in your _plugins folder:
module Jekyll
class SiteNavigation < Jekyll::Generator
safe true
priority :lowest
def generate(site)
# First remove all invisible items (default: nil = show in nav)
sorted = []
site.pages.each do |page|
sorted << page if page.data["navigation"]["show"] != false
end
# Then sort em according to weight
sorted = sorted.sort{ |a,b| a.data["navigation"]["weight"] <=> b.data["navigation"]["weight"] }
# Debug info.
puts "Sorted resulting navigation: (use site.config['sorted_navigation']) "
sorted.each do |p|
puts p.inspect
end
# Access this in Liquid using: site.navigation
site.config["navigation"] = sorted
end
end
end
I've spent quite a while figuring this out since I'm quite new to Jekyll and Ruby, so it would be great if anyone can improve on this.

I can get the code below works on with Jekyll/Liquid match to your requirement with category:
creates links to all existing pages,
sorted by weight (works as well on sorting per category),
highlight the current page.
On top of them it shows also number of post. All is done without any plug-in.
<ul class="topics">
{% capture tags %}
{% for tag in site.categories %}
{{ tag[0] }}
{% endfor %}
{% endcapture %}
{% assign sortedtags = tags | split:' ' | sort %}
{% for tag in sortedtags %}
<li class="topic-header"><b>{{ tag }} ({{ site.categories[tag] | size }} topics)</b>
<ul class='subnavlist'>
{% assign posts = site.categories[tag] | sort:"weight" %}
{% for post in posts %}
<li class='recipe {% if post.url == page.url %}active{% endif %}'>
{{ post.title }}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
Check it on action on our networking page. You may click a post to highlight the navigation, as well a given link to bring you to the source page where their weight is assigned.

If you're trying to sort by weight and by tag and limit the number to 10, here's code to do it:
{% assign counter = '0' %}
{% assign pages = site.pages | sort: "weight" %}
{% for page in pages %}
{% for tag in page.tags %}
{% if tag == "Getting Started" and counter < '9' %}
{% capture counter %}{{ counter | plus:'1' }}{% endcapture %}
<li>{{page.title}}</li>
{% endif %}
{% endfor %}
{% endfor %}

The solution above by #kikito also worked for me. I just added a few lines to remove pages without weight from the navigation and to get rid of white space:
<nav>
<ul>
{% for weight in (1..5) %}
{% unless p.weight %}
{% for p in site.pages %}
{% if p.weight == weight %}
{% if p.url == page.url %}
<li>{{ p.title }}</li>
{% else %}
<li>{{ p.title }}</li>
{% endif %}
{% endif %}
{% endfor %}
{% endunless %}
{% endfor %}
</ul>
</nav>

Related

Jekyll/Liquid Relative URL filter breaks links

I'm trying to use relative_url in most of the links of my Jekyll theme, so if someone wants to have this theme working in a subdirectory he can do it.
I have a problem with the list of categories of the post, each of which should link to the archive.
In _layouts/post.html I have this code:
{% if site.data.settings.categories.active %}
{% include categories.html %}
{% endif %}
categories.html has this code:
<div class="categories">
<span><p>Categories:</p>
{% if post %}
{% assign categories = post.categories %}
{% else %}
{% assign categories = page.categories %}
{% endif %}
{% for category in categories %}
{{category}}
{% unless forloop.last %} {% endunless %}
{% endfor %}
</span>
</div>
Here's the problem:
{{category}}
Somehow, this returns the current post url.
{{category}}
This returns the correct link, but does not work in case the site is in a subdirectory.
Why it returns the post url?
There are multiple problems here.
First off, Liquid doesn't evaluate nested constructs.
Therefore, the following code:
{{ "/categories/#{{category | slugify}}" | relative_url}}
needs to be rewritten into:
{% capture url %}/categories/{{ category | slugify }}{% endcapture %}
{{ url | relative_url }}
Secondly, there is no global post object. Therefore {% if post %} is always going to evaluate to a negative. i.e., it is redundant.

List non-posts pages in relevant category pages

I have a Jekyll blog up and running, but I'd like to do something that seems a bit unorthodox.
Essentially, I've got blog posts in /_posts, and a set of static pages in another folder at /projects.
The top of these projects page looks like this:
---
layout: project
title: My cool project
categories:
- Data Journalism
status: active
---
Now, the biggie: Each category (such as Data Journalism in the example above) has a page with a unique URL, where posts belonging to this category are aggregated.
I would like projects belonging to these categories to be aggregated on these same pages, eg the Data Journalism category page would have a list of projects and a list of posts, all of which are part of this category.
I managed to list all the projects on each category page with
<ul class="posts">
{% for page in site.pages %}
{% if page.layout == 'project' %}
{% if page.status == 'active' %}
<h2>{{ page.title }}</h2>
<div> {{ page.description }}</div>
{% endif %}
{% endif %}
{% endfor %}
</ul>
added in _layouts/category_index.html. But it shows all projects, not only the one belonging to the page category. In the example, My cool project should only be listed in the Data Journalism category page.
Can you help? I'd be most grateful!
EDIT: Thanks to Davic Jacquel for the solution. His answer's below, marked as solved, and here is the tweak I had to make:
<ul class="posts">
{% for p in site.pages %}
{% if p.layout == 'project' and p.status == 'active' %}
{% assign cat = p.categories | downcase %}
{% if cat contains page.category %}
<h2>{{ p.title }}</h2>
<div> {{ p.description }}</div>
{% endif %}
{% endif %}
{% endfor %}
</ul>
Edit : reading your plugin code makes me realize that you have a page.category variable in all your categories pages.
Note : current page's data are stored in the page variable. So, when you loop, try not use page as a storage variable to avoid collision.
In _layouts/category_index.html :
---
layout: default
---
<ul class="posts">
{% for p in site.pages %}
{% if p.layout == 'project' and p.status == 'active' %}
{% if p.categories contains page.category %}
<h2>{{ p.title }}</h2>
<div> {{ p.description }}</div>
{% endif %}
{% endif %}
{% endfor %}
</ul>

In Jekyll, how can I get all pages with a specific tag and limit the results to 3?

In Jekyll, I want to create a homepage showing various categories of pages. For each category, I only want to show 5 pages. How can limit the page results to just 5 per category?
Here's the code I currently have for one of the category sections:
<ul>
{% for page in site.pages %}
{% for tag in page.tags %}
{% if tag == "news" %}
<li><a href="{{ page.permalink | prepend: site.baseurl }}">{{page.title}}
</a></li>
<div class="summary">{{page.summary}}</div>
{% endif %}
{% endfor %}
{% endfor %}
</ul>enter code here
If I add limit:5 in the for page loop, it only looks at 5 pages -- it doesn't look at 5 pages with those tags:
<ul>
{% for page in site.pages limit:5 %}
{% for tag in page.tags %}
...
If I add limit: 5 on the page.tags line, it doesn't do anything:
<ul>
{% for page in site.pages %}
{% for tag in page.tags limit:5 %}
...
I want to avoid having one category that has a lot of pages from taking up too much space. Any ideas?
You need to loop all pages, and count how many with the right category you already found:
{% assign counter = '0' %}
{% for page in site.pages %}
{% for tag in page.tags %}
{% if tag == "news" and counter < '5' %}
{% capture counter %}{{ counter | plus:'1' }}{% endcapture %}
<li><a href="{{ page.permalink | prepend: site.baseurl }}">{{page.title}}
</a> ({{ counter }})</li>
<div class="summary">{{page.summary}}</div>
{% endif %}
{% endfor %}
{% endfor %}
(tested with site.posts instead of site.pages on my blog, because I don't have a Jekyll project where the pages have tags/categories)
The syntax to increase the variable looks a bit ugly, but apparently increasing numeric variables in Liquid isn't that easy (this was the first time I tried it myself).
You should look at group_by, which you can use with the assign tag.
<ul>
{% assign authors = site.posts | group_by: "author" %}
{% for author in authors %}
<li>{{ author.name }}<ul>
{% for page in author.items limit: 5 %}
<li>{{ page.title }}</li>
{% endfor %}
</ul></li>
{% endfor %}
</ul>

In Jekyll How Do I Work With a Specific Post?

I have a sidebar in my site that I want to be able to highlight a specific post.
How do I work with a specific post?
I tried this:
{% assign workingPost = site.posts.2014-09-08-We-Convert-All-Dollars-to-Bitcoin %}
<div>{{workingPost.title}}</div>
But the title is blank. The title for that post is We Convert All Dollars to Bitcoin and that was what I expected to get.
You can do this for all pages with :
<ul class="nav navbar-nav">
{% for p in site.pages %}
<li{% if p.url == page.url %} class="active"{% endif %}>
{{ p.title }}
</li>
{% endfor %}
</ul>
Note: in your answer, you for loop may collides with page variable if you do this in post or page page.
That's why I use {% for p in site.pages %} instead of {% for page in site.pages %}
In this blog post a commentator is looping over the collection and comparing titles. Seems lame, but works for now.
{% for page in site.pages %}
{% if page.title == 'About' %}
{{ page.content }}
{% endif %}
{% endfor %}

How to limit posts filtered by front matter?

I have Jekyll blog where some posts have "featured images," some posts don't.
Featured images are defined in the front matter of the post like so: featured-image: http://path/to/img
On the archive page, I'd like to grab the three most recent posts that have featured images and display them.
I imagine this would need an if statement, a counter, and a loop, but I can't get this one to work for me:
<ul id="archive-featured">
{% assign count = '0' %}
{% if count < '4' %}
{% for post in site.posts %}
{% if post.featured-image == true %}
{{ count | plus: '1' }}
<li><img src="{{post.featured-image}}" />{{ post.title }}</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
What am I missing?
Your YAML matter looks good, but I am not sure you need the count assignment to make this work. Try using limit instead to restrict the number of posts your assign on your archive page. You also do not need to assign a "true" value for the {% if %} statement to work:
<ul id="archive-featured">
{% for post in site.posts limit:3 %}
{% if post.featured-image %}
<li>
<a href="{{ post.url }}">
<img src="{{ post.featured-image }}" />
{{ post.title }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
I believe that the posts are automatically displayed by most recent post, so no extra work needs to be done there. Hope this helps!
A very late answer:
I think the problem is with {{count | plus: 1}}. Wouldn't this just ouput the count + 1, but not assign it?
You can solve this like by assign a new variable before the end of the forloop
<ul id="archive-featured">
{% assign count = 0 %}
{% if count < 4 %}
{% for post in site.posts %}
{% if post.featured-image == true %}
<li><img src="{{post.featured-image}}" />{{ post.title }}</li>
{% count | plus: 1 %}
{% endif %}
{% endfor %}
{% endif %}
</ul>
A workarounds that might be interesting:
If you add another simple statement to your front matter, like featured: true you can use the where filter to select only those posts. (The where filter sadly doesn't seem to work with comparisons)
<ul id="archive-featured">
{% assign posts=site.posts | where "featured", "true" %}
{% for post in posts | limit: 3%}
<li><img src="{{post.featured-image}}" />{{ post.title }}</li>
{% endfor %}
</ul>