Preface
Goal: Apply loop with Liquid to make custom archive pages.
Source Code
This article use tutor-03 theme. We will create it step by step.
Layout Preview for Tutor 03
1: Populate the Content
This is required for loop demo.
Subgoal: Populate content for use with special custom index pages
As always, populate content with good reading is, as hard as giving meaningful variable. Luckily I have done it for you.
Custom Index Page
We are going to explore custom index page such as:
-
Archive
-
Tags and Categories
-
Blog (Article List)
For this to work, we need a proper blog content, with such diversity such as posting year, and tags for each post, so we can represent the list nicely, with sorting, and also grouping whenever necessary.
My content choice comes to song lyrics
.
We can tag song lyrics
with its genre,
and naturally song lyrics
also has date released.
I the also add unique post, to examine what does it looks, to have entirely different folder category.
Configuration
I also add permalink
configuration.
# Produces a cleaner folder structure when using categories
# permalink: /:year/:month/:title.html
permalink: /:categories/:year/:month/:day/:title:output_ext
Pattern
Our typical content is usually short quote from a lyric, with four frontmatter items as below:
---
layout : post
title : Julien Baker - Sprained Ankle
date : 2018-09-13 07:35:05 +0700
categories : lyric
tags : [rock, 2010s]
author : Julien Baker
excerpt: Wish I could write songs about anything other than death
---
A sprinter learning to wait
A marathon runner, my ankles are sprained
A marathon runner, my ankles are sprained
All example content should follow the pattern above.
All these content wear post
layout.
Directory Tree: Posts
A few content is enough. I add ten lyrics to make sure the content have enough diversity.
$ tree _posts/
_posts/
├── 2016-01-01-winter.md
└── lyrics
├── 2017-03-15-nicole-atkins-a-litle-crazy.md
├── 2017-03-25-nicole-atkins-a-night-of-serious-drinking.md
├── 2018-01-15-emily-king-distance.md
├── 2018-02-15-emma-ruth-rundle-shadows-of-my-name.md
├── 2018-09-07-julien-baker-something.md
├── 2018-09-13-julien-baker-sprained-ankle.md
├── 2019-03-15-hemming-vitamins.md
├── 2019-05-15-brooke-annibale-by-your-side.md
├── 2019-05-25-brooke-annibale-yours-and-mine.md
├── 2019-07-15-mothers-no-crying-in-baseball.md
└── 2020-03-15-company-of-thieves-oscar-wilde.md
1 directory, 12 files
Also edit the frontmatter as necessary, for the rest of the content.
Rendered Posts
How does it looks in your file systems?
$ tree _site/lyric
_site/lyric
├── 2017
│ └── 03
│ ├── 15
│ │ └── nicole-atkins-a-litle-crazy.html
│ └── 25
│ └── nicole-atkins-a-night-of-serious-drinking.html
├── 2018
│ ├── 01
│ │ └── 15
│ │ └── emily-king-distance.html
│ ├── 02
│ │ └── 15
│ │ └── emma-ruth-rundle-shadows-of-my-name.html
│ └── 09
│ ├── 07
│ │ └── julien-baker-something.html
│ └── 13
│ └── julien-baker-sprained-ankle.html
├── 2019
│ ├── 03
│ │ └── 15
│ │ └── hemming-vitamins.html
│ ├── 05
│ │ ├── 15
│ │ │ └── brooke-annibale-by-your-side.html
│ │ └── 25
│ │ └── brooke-annibale-yours-and-mine.html
│ └── 07
│ └── 15
│ └── mothers-no-crying-in-baseball.html
└── 2020
└── 03
└── 15
└── company-of-thieves-oscar-wilde.html
23 directories, 11 files
2: Includes: Refactoring
Layout: Header
We need to modify the header a little, so we have simple navigation in plain HTML.
<p>
[ <a href="/">Home</a> ]
[ <a href="/pages/">Blog</a> ]
[ <a href="/pages/about">About</a> ]
[ <a href="/tags/">Tags</a> ]
[ <a href="/categories/">Categories</a> ]
[ <a href="/by-year/">By Year</a> ]
</p>
<hr/>
Includes: Site Directory
Also prepare for later refactoring, we need to put those three files in their own folder.
$ tree _includes
_includes
└── site
├── footer.html
├── header.html
└── head.html
1 directory, 3 files
Layout: Liquid Default
Repecstively, we need to modify includes
toi reflect the right path.
<!DOCTYPE html>
<html>
<head>
{% include site/head.html %}
</head>
<body>
{% include site/header.html %}
<main role="main">
{{ content }}
</main>
{% include site/footer.html %}
</body>
</html>
3: Content: Index
There is no significance changes for index.html
,
except you can change the name to blog.html
,
and add the right permalink.
---
layout : page
title : Blog Posts
permalink : /pages/
---
{% assign posts = site.posts %}
<ul>
{% for post in posts %}
<li>
<a href="{{ site.baseurl }}{{ post.url }}">
{{ post.title }}
</a>
</li>
{% endfor %}
</ul>
Now see how it looks like this page with new content.
Filename Caveat
There is caveat of using blog.html
.
Jekyll pagination-v1
would only work with index.html
.
This issue has been fixed with pagination-v2
.
So for safety issue, we use index.html
instead,
but leave the /pages/
permalink,
just instead we decide to use pagination-v2
,
and rename the file to blog.html
in later chapter.
4: Content: Category and Tag
On most SSG, dealing with tags and categories, need understanding of its data structures.
Content: Frontmatter
Example of Tags and categories in content is as below:
---
layout : post
title : Julien Baker - Sprained Ankle
date : 2018-09-13 07:35:05 +0700
categories : lyric
tags : [rock, 2010s]
---
Data Structure
Both category
and tag
have very similar structure.
While tag use site.tags
, category use site.categories
.
The issue is extracting the tags collection.
In Jekyll, site.tags
is actually a hash (key+value),
that contains array of posts related to that tag.
You can print with related with soul
tag
{{ site.tags['soul'] }}
And check how many post related with that soul
tag
{{ site.tags['soul'] | size }}
In liquid site.tags
can be presented as an array of two:
tag[0]
contain tag name.tag[1]
contain array of posts.
You can check yourself with this snippet.
{% assign tag = site.tags | first %}
{{ tag[0] }}
{{ tag[1] }}
Building Array of Terms
Now all we have to do is to create an array contain all unique posts.
Since liquid
has no built in way to create array,
we have to use split:
{% assign term_array = "" | split: "|" %}
To avoid useless whitespace,
we put the code between capture
tag.
{% capture spaceless %}
...
{% endcapture %}
And finally we have the code
{% capture spaceless %}
{% assign term_array = "" | split: "|" %}
{% for tag in terms %}
{% assign term_first = tag | first %}
{% assign term_array = term_array | push: term_first %}
{% endfor %}
{% assign term_array = term_array | sort %}
{% endcapture %}
I know this code above looks odd. With ruby filter for liquid we can make it shorter as below:
def term_array(terms)
terms.keys
end
But we won’t be using any filter in this chapter.
Tags
Now we can apply above snippets to custom tags pages.
---
layout : page
title : All Tags
permalink : /tags/
---
{% assign terms = site.tags %}
{% capture spaceless %}
{% assign term_array = "" | split: "|" %}
{% for tag in terms %}
{% assign term_first = tag | first %}
{% assign term_array = term_array | push: term_first %}
{% endfor %}
{% assign term_array = term_array | sort %}
{% endcapture %}
<p>Tag List:
<ul>
{% for item in (0..terms.size) %}{% unless forloop.last %}
{% assign this_word = term_array[item] | strip_newlines %}
<li id="{{ this_word | slugify }}" class ="anchor-target">
{{ this_word }}
</li>
{% endunless %}{% endfor %}
</ul>
</p>
The forloop
reference can be read here:
Categories
---
layout : page
title : All Categories
permalink : /categories/
---
{% assign terms = site.categories %}
{% capture spaceless %}
{% assign term_array = "" | split: "|" %}
{% for tag in terms %}
{% assign term_first = tag | first %}
{% assign term_array = term_array | push: term_first %}
{% endfor %}
{% assign term_array = term_array | sort %}
{% endcapture %}
<p>Tag List:
<ul>
{% for item in (0..terms.size) %}{% unless forloop.last %}
{% assign this_word = term_array[item] | strip_newlines %}
<li id="{{ this_word | slugify }}" class ="anchor-target">
{{ this_word }}
</li>
{% endunless %}{% endfor %}
</ul>
</p>
5: Content: Archive by Year
Grouping
Instead of showing list of pages in plain fashioned,
we can show list of pages, grouped by year
, or even by year+month
.
To do this we utilize group_by_exp
filter.
{% assign postsByYear = site.posts
| group_by_exp: "post", "post.date | date: '%Y'" %}
With code above we can loop to show each group, and then having inner loop contain each posts
{% assign postsByYear = site.posts
| group_by_exp: "post", "post.date | date: '%Y'" %}
<div id="archive">
{% for year in postsByYear %}
<section>
<p class ="anchor-target"
id="{{ year.name }}"
>{{ year.name }}</p>
<ul>
{% for post in year.items %}
<li><a href="{{ site.baseurl }}{{ post.url }}">
{{ post.title }}
</a></li>
{% endfor %}
</ul>
</section>
{% endfor %}
</div>
Year Text
We can creatively change the text to show nicer name such as This year’s posts (2020) instead just 2020.
{% capture spaceless %}
{% assign current_year = 'now' | date: '%Y' %}
{% assign year_text = nil %}
{% if year.name == current_year %}
{% assign year_text = year.name
| prepend: "This year's posts (" | append: ')' %}
{% else %}
{% assign year_text = year.name %}
{% endif %}
{% endcapture %}
With plugin, this could be as short as:
def text_year(post_year)
(post_year == Time.now.strftime("%Y")) ?
"This year's posts (#{post_year})" : post_year
end
But we won’t be using plugin in this chapter.
Finally
The complete code is as below:
---
layout : page
title : Archive by Year
permalink : /by-year/
---
{% assign postsByYear = site.posts
| group_by_exp: "post", "post.date | date: '%Y'" %}
<div id="archive">
{% for year in postsByYear %}
{% capture spaceless %}
{% assign current_year = 'now' | date: '%Y' %}
{% assign year_text = nil %}
{% if year.name == current_year %}
{% assign year_text = year.name
| prepend: "This year's posts (" | append: ')' %}
{% else %}
{% assign year_text = year.name %}
{% endif %}
{% endcapture %}
<section>
<p class ="anchor-target"
id="{{ year.name }}"
>{{ year_text }}</p>
<ul>
{% for post in year.items %}
<li><a href="{{ site.baseurl }}{{ post.url }}">
{{ post.title }}
</a></li>
{% endfor %}
</ul>
</section>
{% endfor %}
</div>
You can compare with lyrics directory tree above.
What’s Next?
Consider continue reading [ Jekyll - Plain - Custom Output ].
Thank you for reading.