ssg  
Article Series

Jekyll in General

Jekyll Plain

Where to Discuss?

Local Group

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/

Jekyll: Tree Build URL


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 %}

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)] 
      }]

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.

Jekyll Pagination: Number Pagination without Navigation

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>

Jekyll Pagination: Paginator Using Filter Plugin


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 %}

Jekyll Pagination: Liquid: page_current_flag

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

Jekyll Pagination: Plugin: is_show_adjacent

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>

Jekyll Pagination: Filter: is_show_adjacent

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:

Jekyll Pagination: Combined Animation

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">&laquo;</a> ]
    {% else %}
      [ &laquo; ]
    {% 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 %}
        [ &hellip; ]
      {% 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 %}
        [ &hellip; ]
      {% 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">&raquo;</a> ]
    {% else %}
      [ &raquo; ]
    {% 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.