Navigation
If your Jekyll site has a lot of pages, you might want to create navigation for the pages. Instead of hard-coding navigation links, you can programmatically retrieve a list of pages to build the navigation for your site.
Although there’s already information about interacting with data files in other Jekyll docs, this tutorial dives into building more robust navigation for your site.
There are two primary ways of retrieving pages on a Jekyll site:
- Retrieve pages listed in a YAML data source. Store the page data in a YAML (or JSON or CSV) file in the
_data
folder, loop through the YAML properties, and insert the values into your theme. - Retrieve pages by looping through the page front matter. Look through the front matter of your pages to identify certain properties, return those pages, and insert the pages’ front matter values into your theme.
The examples that follow start with a basic navigation scenario and add more sophisticated elements to demonstrate different ways of returning the pages. In every scenario, you’ll see 3 elements:
- YAML
- Liquid
- Result
The YAML file in the _data
directory is called samplelist.yml
.
The scenarios are as follows:
- Scenario 1: Basic List
- Scenario 2: Sorted list
- Scenario 3: Two-level navigation list
- Scenario 4: Three-level navigation list
- Scenario 5: Using a page variable to select the YAML list
- Scenario 6: Applying the active class for the current page
- Scenario 7: Including items conditionally
- Scenario 8: Retrieving items based on front matter properties
- Scenario 9: Nested tree navigation with recursion
Scenario 1: Basic List
You want to return a basic list of pages.
YAML
docs_list_title: ACME Documentation
docs:
- title: Introduction
url: introduction.html
- title: Configuration
url: configuration.html
- title: Deployment
url: deployment.html
Liquid
<h2>{{ site.data.samplelist.docs_list_title }}</h2>
<ul>
{% for item in site.data.samplelist.docs %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
Result
ACME Documentation
For the results in these fictitious samples, #
is manually substituted for the actual link value (to avoid 404 errors.)
When you use a for
loop, you choose how you want to refer to the items you’re looping through. The variable you choose (in this case, item
) becomes how you access the properties of each item in the list. Dot notation is used to get a property of the item (for example, item.url
).
The YAML content has two main types of formats that are relevant here:
- mapping
- list
docs_list_title: ACME Documentation
is a mapping. You access the value with site.data.samplelist.docs_list_title
.
docs:
is a list. The list begins each item with a hyphen. Unlike with mappings, you usually don’t access list properties directly as you do with mappings. If you want to access a specific item in the list, you must identify the position in the list you want, following typical array notation. For example, site.data.samplelist.docs[0]
would access the first item in the list. However, this is rarely done.
With lists, you usually use for
loops to cycle through the list of items and do something with each item. With navigation menus, you usually insert each list item into li
tags based on the navigation structure you’re using in your HTML theme.
Each hyphen (-
) indicates another item in the list. This example just has two properties with each list item: title
and url
. You can include as many properties as you want for each item. The order of properties at each position in the list doesn’t matter.
Scenario 2: Sorted list
Suppose you wanted to sort the list by the title
. To do this, convert the reference to the docs
collection to a variable, and then apply Liquid’s sort
filter to the variable:
Liquid
{% assign doclist = site.data.samplelist.docs | sort: 'title' %}
<ol>
{% for item in doclist %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ol>
Result
The items now appear in alphabetical order. The sort
property in the Liquid filter applies to the title
, which is an actual property in the list. If title
weren’t a property, we would need to sort by another property.
See the Liquid array filter for more filter options. Note that you can’t simply use this syntax:
{% for item in site.data.samplelist.docs | sort: "title" %}{% endfor %}
You have to convert site.data.samplelist.docs
to a variable first using either assign
or capture
tags.
Scenario 3: Two-level navigation list
Suppose you want a more robust list that incorporates multiple sections of heading titles and subitems. To do this, add an additional level to each list item to store this information:
YAML
toc:
- title: Group 1
subfolderitems:
- page: Thing 1
url: /thing1.html
- page: Thing 2
url: /thing2.html
- page: Thing 3
url: /thing3.html
- title: Group 2
subfolderitems:
- page: Piece 1
url: /piece1.html
- page: Piece 2
url: /piece2.html
- page: Piece 3
url: /piece3.html
- title: Group 3
subfolderitems:
- page: Widget 1
url: /widget1.html
- page: Widget 2
url: /widget2.html
- page: Widget 3
url: /widget3.html
Liquid
{% for item in site.data.samplelist.toc %}
<h3>{{ item.title }}</h3>
<ul>
{% for entry in item.subfolderitems %}
<li><a href="{{ entry.url }}">{{ entry.page }}</a></li>
{% endfor %}
</ul>
{% endfor %}
Result
In this example, Group 1
is the first list item. Within that list item, its subpages are included as a property that itself contains a list (subfolderitems
).
The Liquid code looks through the first level with for item in site.data.samplelist.toc
, and then looks through the second-level property with for entry in item.subfolderitems
. Just as item
is an arbitrary name for the items we’re looping through, so is entry
.
Scenario 4: Three-level navigation list
Building on the previous section, let’s add one more level of depth (subsubfolderitems
) to the list. The formatting will get more complex here, but the principles are the same.
YAML
toc2:
- title: Group 1
subfolderitems:
- page: Thing 1
url: /thing1.html
- page: Thing 2
url: /thing2.html
subsubfolderitems:
- page: Subthing 1
url: /subthing1.html
- page: Subthing 2
url: /subthing2.html
- page: Thing 3
url: /thing3.html
- title: Group 2
subfolderitems:
- page: Piece 1
url: /piece1.html
- page: Piece 2
url: /piece2.html
- page: Piece 3
url: /piece3.html
subsubfolderitems:
- page: Subpiece 1
url: /subpiece1.html
- page: Subpiece2
url: /subpiece2.html
- title: Group 3
subfolderitems:
- page: Widget 1
url: /widget1.html
subsubfolderitems:
- page: Subwidget 1
url: /subwidget1.html
- page: Subwidget 2
url: /subwidget2.html
- page: Widget 2
url: /widget2.html
- page: Widget 3
url: /widget3.html
Liquid
<div>
{% if site.data.samplelist.toc2[0] %}
{% for item in site.data.samplelist.toc2 %}
<h3>{{ item.title }}</h3>
{% if item.subfolderitems[0] %}
<ul>
{% for entry in item.subfolderitems %}
<li><a href="{{ entry.url }}">{{ entry.page }}</a>
{% if entry.subsubfolderitems[0] %}
<ul>
{% for subentry in entry.subsubfolderitems %}
<li><a href="{{ subentry.url }}">{{ subentry.page }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
{% endif %}
</div>
Result
Group 1
Group 2
Group 3
In this example, if site.data.samplelist.toc2[0]
is used to ensure that the YAML level actually contains items. If there isn’t anything at the [0]
position, we can skip looking in this level.
ProTip: Line up for
loops and if
statements
To keep the code clear, line up the beginning and ending Liquid tags, such as the for
loops and if
statements. This way you know when the open tags have been closed. If the code will appear in a Markdown page, keep the opening and closing HTML tags flush against the left edge so that the Markdown filter won't treat the content as a code sample. If necessary, you can wrap the entire code sample in a div
tag to ensure the code has HTML tags that bookend the code.
Scenario 5: Using a page variable to select the YAML list
Suppose your sidebar will differ based on various documentation sets. You might have 3 different products on your site, and so you want 3 different sidebars — each unique for that product.
You can store the name of the sidebar list in your page front matter and then pass that value into the list dynamically.
Page front matter
---
title: My page
sidebar: toc
---
Liquid
<ul>
{% for item in site.data.samplelist[page.sidebar] %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
Result
In this scenario, we want to pass values from the page’s front matter into a for
loop that contains a variable. When the assigned variable isn’t a string but rather a data reference, you must use brackets (instead of curly braces) to refer to the front matter’s value.
For more information, see Expressions and Variables in Liquid’s documentation. Brackets are used in places where dot notation can’t be used. You can also read more details in this Stack Overflow answer.
Scenario 6: Applying the active class for the current page
In addition to inserting items from the YAML data file into your list, you also usually want to highlight the current link if the user is viewing that page. You do this by inserting an active
class for items that match the current page URL.
CSS
.result li.active a {
color: lightgray;
cursor: default;
}
Liquid
{% for item in site.data.samplelist.docs %}
<li class="{% if item.url == page.url %}active{% endif %}">
<a href="{{ item.url }}">{{ item.title }}</a>
</li>
{% endfor %}
Result
In this case, assume Deployment
is the current page.
To make sure the item.url
(stored in the YAML file) matches the page.url
, it can be helpful to print the {{ page.url }}
to the page.
Scenario 7: Including items conditionally
You might want to include items conditionally in your list. For example, maybe you have multiple site outputs and only want to include the sidebar item for certain outputs. You can add properties in each list item and then use those properties to conditionally include the content.
YAML
docs2_list_title: ACME Documentation
docs2:
- title: Introduction
url: introduction.html
version: 1
- title: Configuration
url: configuration.html
version: 1
- title: Deployment
url: deployment.html
version: 2
Liquid
<ul>
{% for item in site.data.samplelist.docs2 %}
{% if item.version == 1 %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endif %}
{% endfor %}
</ul>
Result
The Deployment
page is excluded because its version
is 2
.
Scenario 8: Retrieving items based on front matter properties
If you don’t want to store your navigation items in a YAML file in your _data
folder, you can use for
loops to look through the front matter of each page or collection and get the content based on properties in the front matter.
In this scenario, suppose we have a collection called _docs
. Collections are often better than pages because they allow you to narrow the list of what you’re looping through. (Try to avoid scenarios where you loop through large numbers of items, since it will increase your build time. Collections help you narrow the scope.)
In our scenario, there are 6 docs in the docs
collection: Sample 1, Sample 2, Topic 1, Topic 2, Widget 1, and Widget 2.
Each doc in the collection contains at least 3 properties in the front matter:
title
category
order
The front matter for each page is as follows (consolidated here for brevity):
---
Title: Sample 1
category: getting-started
order: 1
---
---
Title: Sample 2
category: getting-started
order: 2
---
---
Title: Topic 1
category: configuration
order: 1
---
---
Title: Topic 2
category: configuration
order: 2
---
---
Title: Widget 1
category: deployment
order: 1
---
---
Title: Widget 2
category: deployment
order: 2
---
Note that even though category
is used in the doc front matter, category
is not a built-in variable like it is with posts. In other words, you cannot look directly inside category
with site.docs.category
.
If you wanted to simply get all docs in the collection for a specific category, you could use a for
loop with an if
condition to check for a specific category:
<h3>Getting Started</h3>
<ul>
{% for doc in site.docs %}
{% if doc.category == "getting-started" %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endif %}
{% endfor %}
</ul>
The result would be as follows:
This might be useful if you’re setting up a knowledge base and have dozens of topics in each category, with each category displaying on its own page.
But let’s say you want to sort the items by category and group them under the category name, without hard-coding the category names. To achieve this, you could use two filters:
group_by
sort
Here’s the code for getting lists of pages grouped under their corresponding category headers:
Liquid
{% assign mydocs = site.docs | group_by: 'category' %}
{% for cat in mydocs %}
<h2>{{ cat.name | capitalize }}</h2>
<ul>
{% assign items = cat.items | sort: 'order' %}
{% for item in items %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
{% endfor %}
Result
Let’s walk through the code. First, we assign a variable (mydocs
) to the collection content (site.docs
).
The group_by
filter groups the collection content by category
. More specifically, the group_by
filter converts mydocs
into an array with name
, items
, and size
properties, somewhat like this:
[
{"name": "getting-started", "items": [Sample 1, Sample 2],"size": 2},
{"name": "configuration", "items": [Topic 1, Topic 2], "size": 2},
{"name": "deployment", "items": [Widget 1, Widget 2], "size": 2}
]
Using for cat in mydocs
, we look through each item in the mydocs
array and print the category name
.
After getting the category name, we assign the variable items
for the docs and use the sort
filter to arrange the docs by their order
property. The dot notation cat.items
is used because we’re accessing the content in the items
array. The sort
filter orders the items by their numbers in ascending order.
The for item in items
loop looks through each item
and gets the title
and url
to form the list item link.
For more details on the group_by
filter, see Jekyll’s Templates documentation as well as this Siteleaf tutorial. For more details on the sort
filter, see sort in Liquid’s documentation.
Whether you use properties in your doc’s front matter to retrieve your pages or a YAML data file, in both cases you can programmatically build a more robust navigation for your site.
Scenario 9: Nested tree navigation with recursion
Suppose you want a nested tree navigation of any depth. We can achieve this by recursively looping through our tree of navigation links.
YAML
nav:
- title: Deployment
url: deployment.html
subnav:
- title: Heroku
url: heroku.html
subnav:
- title: Jekyll on Heroku
url: jekyll-on-heroku.html
- title: Help
url: help.html
Liquid
First, we’ll create an include that we can use for rendering the navigation tree. This file would be _includes/nav.html
<ul>
{% for item in include.nav %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% if item.subnav %}
{% include nav.html nav=item.subnav %}
{% endif %}
{% endfor %}
</ul>
To render this in your layout or pages, you would simply include the template and pass in the nav
parameter. In this case, we’ll use the page.nav
to grab it from the yaml frontmatter.
{% include nav.html nav=page.nav %}
Our include will use this first, then look through each item for a subnav
property to recursively render the nested lists.
Result