Preface
Goal: Convert complex pagination in liquid code to simple ruby plugin.
Source Code
This article use tutor-06 theme. We will create it step by step.
Introduction
Assumption
Since we write custom plugin,
we can assume that our system have no obstacle to use pagination-v2
.
Hence we can get rid of pagination-v1
.
This have a few impacts.
Content: Index
With pagination-v2
,
We are freely to use any file name such as blog.html
.
and add the right permalink.
The complete setting are here below:
---
layout : blog
title : Blog Posts
permalink : /pages/
# custom frontmatter for both pagination-v1 and pagination-v2
paginate_root : /pages
# official frontmatter pagination-v2
pagination:
enabled: true
---
Configuration
The configuration is also clean, since we have only one option.
And choose not to use paginate-v1
.
# all plugins
plugins:
- jekyll-paginate-v2
# jekyll-paginate-v2
# Pagination Settings
pagination:
enabled : true
per_page : 2
permalink : '/blog-:num/'
title : ':title | :num of :max pages'
limit : 0
sort_field : 'date'
sort_reverse : true
Layout: Blog
The layout blog is still remaining the same.
---
layout: page
---
{% include pagination-v2/04-indicator.html %}
{% assign posts = paginator.posts %}
{% include index/blog-list.html %}
{% comment %}
v2: not supported by github pages (Without travis)
{% include pagination-v2/01-simple.html %}
{% include pagination-v2/02-number.html %}
{% include pagination-v2/03-adjacent.html %}
{% include pagination-v2/04-indicator.html %}
{% endcomment %}
By each step, we still have four choices.
Build Directory
Depend on your settings, with either jekyll pagination, you can achieve these urls, as below example:
- Page 1: http://localhost:4000/pages/
And consecutively
-
Page 2: http://localhost:4000/pages/page-2/
-
Page 3: http://localhost:4000/pages/page-3/
-
Page 4: http://localhost:4000/pages/page-4/
1: Filter: Pagination: Links
If you care, about the simple pagination in liquid, that previous liquid code can summarized as below:
Liquid: Pagination-v2: Numbers
Variable Initializations
{% capture spaceless %}
{% assign total_pages = paginator.total_pages %}
{% assign page_current = paginator.page %}
{% assign paginate_root = page.paginate_root %}
{% endcapture %}
Link Calculations
{% capture spaceless %}
<!-- Get links -->
{% assign p_first = paginate_root
| prepend: site.baseurl %}
{% assign p_prev = paginator.previous_page_path
| prepend: site.baseurl %}
{% assign p_next = paginator.next_page_path
| prepend: site.baseurl %}
{% assign p_last = site.pagination.permalink
| prepend: paginate_root
| relative_url
| replace: ':num', total_pages
{% endcapture %}
Page Numbers in Loop.
<!-- Page numbers. -->
{% for page_cursor in (1..total_pages) %}
{% if page_cursor == page_current %}
[ {{ page_cursor }} ]
{% else %}
{% if page_cursor == 1 %}
{% assign p_link = p_first %}
{% else %}
{% assign p_link = site.pagination.permalink
| prepend: paginate_root
| relative_url
| replace: ':num', page_cursor
%}
{% endif %}
...
{% endif %}
{% endfor %}
Ruby Filter: PaginationLinks
To avoid complex liquid code, we can utilize liquid filter plugin, to calculate any links.
Assuming with plugin we can get rid of pagination-v1
,
then we can make plugin specifically for pagination-v2
.
def pagination_links(paginator, baseurl, paginate_root, permalink)
ppp = paginator['previous_page_path']
npp = paginator['next_page_path']
tp = paginator['total_pages']
_first = baseurl + paginate_root
_other = baseurl + paginate_root + permalink
Hash[
"prev" => baseurl + (ppp ? ppp : ""),
"next" => baseurl + (npp ? npp : ""),
"first" => _first,
"last" => _other.sub(":num", tp.to_s)
]
end
And links for each pages can be calculated by this code below:
tp = paginator['total_pages']
_first = baseurl + paginate_root
_other = baseurl + paginate_root + permalink
pages = Hash.new
(1..tp).each do |i|
pages[i] = (i==1) ? _first : _other.sub(":num", i.to_s)
end
So we have the complete hash as below:
Hash[
"prev" => baseurl + (ppp ? ppp : ""),
"next" => baseurl + (npp ? npp : ""),
"first" => _first,
"last" => _other.sub(":num", tp.to_s),
"pages" => pages
]
If you do not mind to be cryptic,
you can also rewrite the pages
variable to use Hash
directly.
pages = Hash[(1..tp).collect { # key pairs
|i| [i, (i==1) ? _first : _other.sub(":num", i.to_s)]
}]
Ruby Filter: Rewriting PaginationLinks
Remember our URL structure,
this would be clearer if we define in the first place by function.
Or better with lambda in _permalink
variable below.
def pagination_links(paginator, baseurl, paginate_root, permalink)
_total = paginator['total_pages']
_first = baseurl + paginate_root
_permalink = ->(i) { (i==1) ? "" : permalink.sub(":num", i.to_s) }
end
So we can execute as below:
pages = Hash[(1.._total).collect { # key pairs
|i| [i, _first + _permalink.(i)]
}]
Or put them all in the hash
{
"prev" => baseurl + ( paginator['previous_page_path'] || "" ),
"next" => baseurl + ( paginator['next_page_path'] || "" ),
"first" => _first,
"last" => _first + _permalink.(_total),
"pages" => Hash[(1.._total).collect { # key pairs
|i| [i, _first + _permalink.(i)]
}]
}
No we can have our complete code as below:
module Jekyll
module PaginationLinks
def pagination_links(paginator, baseurl, paginate_root, permalink)
_total = paginator['total_pages']
_first = baseurl + paginate_root
_permalink = ->(i) { (i==1) ? "" : permalink.sub(":num", i.to_s) }
{
"prev" => baseurl + ( paginator['previous_page_path'] || "" ),
"next" => baseurl + ( paginator['next_page_path'] || "" ),
"first" => _first,
"last" => _first + _permalink.(_total),
"pages" => Hash[(1.._total).collect { # key pairs
|i| [i, _first + _permalink.(i)]
}]
}
end
end
end
Liquid::Template.register_filter(Jekyll::PaginationLinks)
Using Filter
Consider check the result.
You can pipe this filter as code below:
{% assign links = paginator
| pagination_links: site.baseurl
, page.paginate_root
, site.pagination.permalink
%}
Now the Liquid source in Jekyll should be cleaner.
{% assign total_pages = paginator.total_pages %}
<nav role="navigation">
{% if total_pages > 1 %}
{% capture spaceless %}
{% assign page_current = paginator.page %}
{% assign links = paginator
| pagination_links: site.baseurl
, page.paginate_root
, site.pagination.permalink
%}
{% endcapture %}
<!-- First Page. -->
{% unless paginator.page == 1 %}
[ <a href="{{ links.first }}">First</a> ]
{% else %}
[ First ]
{% endunless %}
<!-- Previous Page. -->
{% if paginator.previous_page %}
[ <a href="{{ links.prev }}">Previous</a> ]
{% else %}
[ Previous ]
{% endif %}
<!-- Page numbers. -->
{% for page in (1..total_pages) %}
{% if page == page_current %}
[ {{ page }} ]
{% else %}
[ <a href="{{ links.pages[page] }}">{{ page }}</a> ]
{% endif %}
{% endfor %}
<!-- Next Page. -->
{% if paginator.next_page %}
[ <a href="{{ links.next }}">Next</a> ]
{% else %}
[ Next ]
{% endif %}
<!-- Last Page. -->
{% unless paginator.page == total_pages %}
[ <a href="{{ links.last }}">Last</a> ]
{% else %}
[ Last ]
{% endunless %}
{% endif %}
</nav>
2: Filter: Pagination: Adjacent Offset
Now the harder part, calculating pagination offset. Decide what to show, and what not to show.
Calculating Flag Value
The long complex calculation
<!-- Page numbers. -->
{% for page_cursor in (1..total_pages) %}
{% capture spaceless %}
<!-- Flag Calculation -->
{% assign page_current_flag = false %}
{% if total_pages > link_max %}
<!-- Complex page numbers. -->
<!-- Lower limit pages. -->
<!-- If the user is on a page which is in the lower limit. -->
{% if page_current <= limit_lower %}
<!-- If the current loop page is less than max_links. -->
{% if page_cursor <= min_lower %}
{% assign page_current_flag = true %}
{% endif %}
<!-- Upper limit pages. -->
<!-- If the user is on a page which is in the upper limit. -->
{% elsif page_current >= limit_upper %}
<!-- If the current loop page is greater than total pages minus $max_links -->
{% if page_cursor > max_upper %}
{% assign page_current_flag = true %}
{% endif %}
<!-- Middle pages. -->
{% else %}
{% if (page_cursor >= lower_offset) and (page_cursor <= upper_offset) %}
{% assign page_current_flag = true %}
{% endif %}
{% endif %}
{% else %}
<!-- Simple page numbers. -->
{% assign page_current_flag = true %}
{% endif %}
{% endcapture %}
<!-- Show Pager. -->
...
{% endfor %}
Wow! What a long, not easy to understand lines of code!
Ruby Filter: IsShowAdjacent
Move complex code to plugin!
For my personal unknown reason, this would be better, if we move complex code from template, to a liquid filter in Ruby. But be aware that since not every CI/CD allow ruby plugin, we will not use this ruby plugin primarily.
Consider give the filter name as is_show_adjacent
.
Now we have direct port from pure liquid to ruby filter plugin.
module Jekyll
module IsShowAdjacent
def is_show_adjacent(cursor, current, total_pages, link_offset)
# initialize show cursor flag
flag = false
# link_offset related variables
max_links = (link_offset * 2) + 1;
lower_limit = 1 + link_offset;
upper_limit = total_pages - link_offset;
if total_pages > max_links
# Complex page numbers.
case
when current <= lower_limit
# Lower limit pages.
# If the user is on a page which is in the lower limit.
flag = true if cursor <= max_links
when current >= upper_limit
# Upper limit pages.
# If the user is on a page which is in the upper limit.
flag = true if cursor > (total_pages - max_links)
else
# Middle pages.
if ( (cursor >= current - link_offset) &&
(cursor <= current + link_offset) )
flag = true
end
end
else
# Simple page numbers.
flag = true
end
flag
end
end
end
Using Filter
Now the Liquid source in Jekyll should be cleaner.
{% assign total_pages = paginator.total_pages %}
<nav role="navigation">
{% if total_pages > 1 %}
{% capture spaceless %}
{% assign link_offset = 2 %}
{% assign page_current = paginator.page %}
{% assign links = paginator
| pagination_links: site.baseurl
, page.paginate_root
, site.pagination.permalink
%}
{% endcapture %}
<!-- Page numbers. -->
{% for page in (1..total_pages) %}
{% capture spaceless %}
<!-- Flag Calculation -->
{% assign page_current_flag = page
| is_show_adjacent: page_current
, total_pages
, link_offset %}
{% endcapture %}
<!-- Show Pager. -->
{% if page_current_flag == true %}
{% if page == page_current %}
[ {{ page }} ]
{% else %}
[ <a href="{{ links.pages[page] }}">{{ page }}</a> ]
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</nav>
You can pipe this filter as code below:
{% assign page_current_flag = page
| is_show_adjacent: page_current
, total_pages
, link_offset %}
This page_current_flag
should be checked in every loop.
Skeleton
To get more understanding about this filter, consider to examine this skeleton below:
def is_show_adjacent(cursor, current, total_pages, link_offset)
# variable initialization
...
if total_pages > max_links
# Complex page numbers.
case
when current <= lower_limit
# Lower limit pages.
...
when current >= upper_limit
# Upper limit pages.
...
else
# Middle pages.
...
end
end
else
# Simple page numbers.
flag = true
end
flag
end
Oneliner Conditional
I care about readibility. But if you wish, you can make the code shorter, and of course cryptic.
# Complex page numbers.
case
when current <= lower_limit then flag = true if cursor <= max_links
when current >= upper_limit then flag = true if cursor > (total_pages - max_links)
else # Middle pages.
if ( (cursor >= current - link_offset) &&
(cursor <= current + link_offset) )
flag = true
end
end
I’d rather to keep the code comments intact, for readability reason.
Using Filter
We can achieve pagination with this code below:
{% assign total_pages = paginator.total_pages %}
<nav role="navigation">
{% if total_pages > 1 %}
{% capture spaceless %}
<!--
Pagination links
* https://glennmccomb.com/articles/how-to-build-custom-hugo-pagination/
-->
{% assign link_offset = 2 %}
{% assign page_current = paginator.page %}
{% assign links = paginator
| pagination_links: site.baseurl
, page.paginate_root
, site.pagination.permalink
%}
{% endcapture %}
<!-- Page numbers. -->
{% for page in (1..total_pages) %}
{% capture spaceless %}
<!-- Flag Calculation -->
{% assign page_current_flag = page
| is_show_adjacent: page_current
, total_pages
, link_offset %}
{% endcapture %}
<!-- Show Pager. -->
{% if page_current_flag == true %}
{% if page == page_current %}
[ {{ page }} ]
{% else %}
[ <a href="{{ links.pages[page] }}">{{ page }}</a> ]
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</nav>
Conclusion
As a Conclusion, let me summarized in complete liquid code below:
Pagination, along with all navigation buttons.
{% assign total_pages = paginator.total_pages %}
<nav role="navigation">
{% if total_pages > 1 %}
{% capture spaceless %}
<!--
Pagination links
* https://glennmccomb.com/articles/how-to-build-custom-hugo-pagination/
-->
{% assign page_current = paginator.page %}
{% assign links = paginator
| pagination_links: site.baseurl
, page.paginate_root
, site.pagination.permalink
%}
{% assign link_offset = 2 %}
{% assign link_max = link_offset | times: 2 | plus: 1 %}
{% assign lower_offset = page_current | minus: link_offset %}
{% assign upper_offset = page_current | plus: link_offset %}
{% assign lower_indicator = 2 %}
{% assign upper_indicator = total_pages | minus: 1 %}
{% endcapture %}
<!-- Previous Page. -->
{% if paginator.previous_page %}
[ <a href="{{ links.prev }}" rel="prev">«</a> ]
{% else %}
[ « ]
{% endif %}
{% if total_pages > link_max %}
<!-- First Page. -->
{% if lower_offset > 1 %}
[ <a href="{{ links.first }}">1</a> ]
{% endif %}
<!-- Early (More Pages) Indicator. -->
{% if lower_offset > lower_indicator %}
[ … ]
{% endif %}
{% endif %}
<!-- Page numbers. -->
{% for page in (1..total_pages) %}
{% capture spaceless %}
<!-- Flag Calculation -->
{% assign page_current_flag = page
| is_show_adjacent: page_current
, total_pages
, link_offset %}
{% endcapture %}
<!-- Show Pager. -->
{% if page_current_flag == true %}
{% if page == page_current %}
[ {{ page }} ]
{% else %}
[ <a href="{{ links.pages[page] }}">{{ page }}</a> ]
{% endif %}
{% endif %}
{% endfor %}
{% if total_pages > link_max %}
<!-- Late (More Pages) Indicator. -->
{% if upper_offset < upper_indicator %}
[ … ]
{% endif %}
<!-- Last Page. -->
{% if upper_offset < total_pages %}
[ <a href="{{ links.last }}">{{ total_pages }}</a> ]
{% endif %}
{% endif %}
<!-- Next Page. -->
{% if paginator.next_page %}
[ <a href="{{ links.next }}" rel="next">»</a> ]
{% else %}
[ » ]
{% endif %}
{% endif %}
</nav>
This code above using both filter:
-
pagination_links
, and -
is_show_adjacent
.
Code using filter is cleaner, than code using pure liquid.
What is Next ?
Consider continue reading [ Jekyll Plain - Plugin - Tag Names ].
Thank you for reading.