For the recently launched Updates section on Web Fundamentals, we sorely needed pagination. The whole site on /web, including all sub sites (i.e. our Showcase, the Shows section, etc.) runs on a single instance of Jekyll, which made said task more difficult than it needed to be. If all you need is pagination for your single Jekyll blog, stop reading and use Jekyll’s built in pagination. But we needed more:
- Pagination across custom page lists
- Pagination that handles filters like categories
- Pagination that can be localized to a certain section
I googled heavily but couldn’t find a solution anywhere else in the blogosphere, so hence I figured you adventurous reader who’s heading here might benefit from the following resources.
_plugins/updates_generator.rb (Live version)
This is the heart of it. This plugin generates all pages across the Updates section, based on a single template. It generates tag pages, product pages, category pages and pagination for most. The following is a simplified version that focusses on a few categories, and strips out tags. Before you dig in, a couple of notes:
- It’s assumed that you have a custom object site.data[‘articles’][‘updates’] that includes all parsed update pages. In our case, that is done by another plugin (of course, this can be any collection)
- It’s also assumed that you’d like to filter your pages by product and category. If you don’t need that, strip it out.
- This is my first venture into Ruby, so lots of this is probably extremely verbose and can be simplified.
- You should probably move the per_page variable into some sort of setting (same with product/category)
module Jekyll
# Generate pagination for update pages
class UpdatesTagPaginator < Generator
priority :low
def generate(site)
# Grab all update pages from custom articles object
updates = site.data['articles']['updates']
# if no updates found, exit here
if updates.nil?
return
end
# generate category/product pages
# todo: add more products/categories here if needed.
categories = ["news", "tip"]
products = ["chrome", "chrome-devtools"]
# generate /category and /product/category pages
categories.each do |category|
generatePaginatedPage(site, site.source, File.join('updates', category), category, "all")
products.each do |product|
generatePaginatedPage(site, site.source, File.join('updates', product, category), category, product)
end
end
# generate /product pages
products.each do |product|
generatePaginatedPage(site, site.source, File.join('updates', product), "all", product)
end
# generate main page
generatePaginatedPage(site, site.source, File.join('updates'), "all", "all")
end
def generatePaginatedPage(site, base, dir, category, product)
pag_root = dir
# Change this so it matches your src/ folder structure. We use translations, heck the following.
dir = File.join('_langs', site.data['curr_lang'], dir)
# override this (or put into a setting). This is the number of articles per paginated page
per_page = 10
# filter array so it only contains what we need
updates = site.data['articles']['updates']
updates = updates.select do |update|
(category == "all" || update["category"] == category) && (product == "all" || update["product"] == product)
end
updates = updates.sort { |x,y| y["date"] <=> x["date"] }
page_count = updates.count <= per_page ? 1 : calculate_pages(updates, per_page)
(1..page_count).each do |num_page|
# generate first page
site.pages << UpdatesPage.new(site, base, dir, category, product, updates[0..per_page-1], page_count, 1, pag_root) if num_page > 1
# generate all other paginated pages
start = (num_page - 1) * per_page
num = (start + per_page - 1) >= updates.size ? updates.size : (start + per_page - 1)
site.pages << UpdatesSubPage.new(site, base, File.join(dir, num_page.to_s), category, product, updates[start..num], page_count, num_page, pag_root)
end
end
end
def calculate_pages(updates, per_page)
(updates.size.to_f / per_page.to_i).ceil
end
end
class UpdatesPage < Page
attr_accessor :tag
def initialize(site, base, dir, category, product, updates, pag_total, pag_current, pag_root)
@site = site
@base = base
@dir = dir
@name = "index.html"
self.process(@name)
self.read_yaml(File.join(base, '_layouts'), 'updates.liquid')
self.data['category'] = category
self.data['product'] = product
self.data['updates'] = updates
self.data['pagination_total'] = pag_total
self.data['pagination_current'] = pag_current
self.data['pagination_root'] = pag_root
end
end
end
Here is the live version that includes tag pages.
_layouts/updates.liquid (Live version)
This layout is used by the generator. Since we do all the filtering in Ruby, we don’t need a lot of logic in it anymore! Isn’t that nice? Notes:
- This is a heavily stripped down version that only does a simple title-based linked list on each page.
- Note how you don’t need to filter anything here: page.updates already contains the paginated, pre-filtered list of updates.
---
layout: default
collection: web
published: true
product: all
category: all
title: Web Updates
---
{% assign updates = page.updates | sort: 'date' | reverse %}
<ul>
{% for article in updates %}
<li class="{{article.type}}">
<a href="{{article.url | canonicalize}}">
<h3>{{article.title}}</h3>
</a>
</li>
{% endfor %}
</ul>
{% if page.pagination_total > 1 %}
<div class="container updates-pagination">
<ul>
<li class="prev">
{% if page.pagination_current == 1 %}
<
{% else %}
<a href="/web/{{ page.pagination_root }}{% if page.pagination_current != 2 %}/{{ page.pagination_current | minus: 1 }}{% endif %}"><</a>
{% endif %}
</li>
{% if page.pagination_total < 8 %} {% for i in (1..page.pagination_total) %} <li{% if i == page.pagination_current %} class="current"{% endif %}>
<a href="/web/{{ page.pagination_root }}{% if i != 1 %}/{{i}}{% endif %}">{{ i }}</a>
</li>
{% endfor %}
{% else %}
{% for i in (1..5) %}
<li{% if i == page.pagination_current %} class="current"{% endif %}>
<a href="/web/{{ page.pagination_root }}{% if i != 1 %}/{{i}}{% endif %}">{{ i }}</a>
</li>
{% endfor %}
<li class="truncated">...</li>
<li{% if page.pagination_total == page.pagination_current %} class="current"{% endif %}>
<a href="/web/{{ page.pagination_root }}/{{page.pagination_total}}">{{ page.pagination_total }}</a>
</li>
{% endif %}
<li class="next">
{% if page.pagination_current == page.pagination_total %}
>
{% else %}
<a href="/web/{{ page.pagination_root }}/{{ page.pagination_current | plus: 1 }}">></a>
{% endif %}
</li>
</ul>
</div>
{% endif %}
That’s it! Modify to your needs and live long and prosper.