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 %}">&lt;</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 }}">&gt;</a>
    {% endif %}
    </li>

  </ul>
</div>
{% endif %}

That’s it! Modify to your needs and live long and prosper.