coffeebrew_jekyll_paginate

coffeebrew_jekyll_paginate

A Jekyll plugin to generate site pagination.

Jekyll Paginate Plugin

A Jekyll plugin to generate site pagination for collections.

Installation

Add this line to your site's Gemfile:

gem 'coffeebrew_jekyll_paginate'

And then add this line to your site's _config.yml:

plugins:
  - coffeebrew_jekyll_paginate

By default, the plugin doesn't generate any pagination. You need to specify which collections to be paginated. For example,

---
coffeebrew_jekyll_paginate:
  collections:
    books:

Assuming layouts have been setup (see more in Layouts), then, the plugin will generate paginated collections using the default config below:

individual_page_pagination: false
frontmatter_defaults_key: "paginated_%{collection_type}"
first_page_as_root:
  enabled: false
permalink: /:collection_type/:page_num_one_index
index_page: "index"
per_page: 5
sort_field: "date"
sort_reverse: true
page_num_label: "%{page_num_one_index}"

An example of the generated directory structure is as such:

_site/
├── books
│   ├── 1
│   │   ├── index.html
│   └── 2
│       └── index.html
└── index.html

Configuration

You can configure for as many collections as needed, and configure a site-wide pagination config, or configure for each collection to use its own configuration. If there is no configuration for the site default, or the collection, the plugin will use the default values as previously mentioned.

Site-wide defaults

For example, you can configure site-wide defaults that override the plugin's defaults, which will be used for all the collections enabled.

coffeebrew_jekyll_paginate:
  defaults:
    individual_page_pagination: true
    first_page_as_root:
      enabled: true
      permalink: /
      index_page: /:collection_type
    per_page: 3
    page_num_label: "Page %{page_num_one_index}"
  collections:
    books:
    posts:

If you do override the configuration, the plugin will perform a simple validation on your overrides according to these rules:

Frontmatter defaults key config

This tells the plugin what is the key used for setting defaults. For example, the plugin will use paginated_posts for posts type by default, and this will allow Jekyll to lookup defaults for type: "paginated_posts" in _config.yml.

defaults:
  - scope:
      path: ""
      type: "paginated_posts"
    values:
      layout: "posts_index"

Individual page pagination config

If set to true, this tells the plugin to generate previous/next pagination for individual collection pages. This is useful if you want to have individual page-level pagination to navigate to the previous or next page. For example:

---
layout: default
---
<h1>Posts</h1>
{% raw %}
{{ content }}

<div class="pagination">
  <ul class="pager">
    <li class="previous">
      <a href="{{ page.paginator.previous_page_path }}">< {{ page.paginator.previous_page.title }}</a>
    </li>
    <li class="next">
      <a href="{{ page.paginator.next_page_path }}">{{ page.paginator.next_page.title }} ></a>
    </li>
  </ul>
</div>
{% endraw %}

First page as root config

This tells the plugin to generate the first page outside of the collection directory in the paginated set. For example, if there are 4 pages in the paginated books collection, then the first page will be rendered in /books.html, and the other pages will be rendered as /books/2/index.html, /books/3/index.html and /books/4/index.html.

This will be useful if you want to have the first page as part of your root pages and include it in the navigation.

Key Allowed Value(s) Default Remark
enabled Boolean false If set to true, then the plugin will render the first page in a separate path as defined by the permalink and index_page below.
permalink String nil The directory in which to generate the first page. Normally this will be / if you want it to be a root-level page.
index_page String nil The filename of the first page. Normally this will be the name corresponding to the collection.

If enabled is false, both permalink and index_page will be ignored, and the plugin will generate the first page the same way as the remaining pages.

Pagination config

This tells the plugin how to generate the pagination pages.

Key Allowed Value(s) Default Remark
permalink String /:collection_type/:page_num_one_index The directory in which to generate the page.
index_page String index The filename of the page.
per_page Integer 5 The number of collection items in each page.
sort_field String date The field to be used to sort the collection for deterministic pagination.
sort_reverse Boolean true The collection will be sorted in reverse if set to true.
page_num_label String %{page_num_one_index} The format string for the page number label.

A few placeholders are available to be used in the permalink, index_page and page_num_label:

Field Description
collection_type This is the collection type current page, eg. posts.
page_num_zero_index This will be the 0-index page number of the current page.
page_num_one_index This will be the 1-index page number of the current page.

Validation

If the config overrides have invalid structure, keys or values, the plugin will raise a Jekyll::Errors::InvalidConfigurationError during build.

Layouts

The plugin does not provide a default layout. You will need to create your own layout in _layouts and configure the defaults in _config.yml, for example, if you want to paginate the books collection, then you need to configure the layout for the collection:

---
defaults:
  - scope:
      type: "paginated_books"
    values:
      layout: "books_pagination"
  - scope:
      type: "books"
    values:
      layout: "book"
      permalink: /books/:name:output_ext

As mentioned earlier, you need to set the scope.type here to match the frontmatters_defaults_key config of the plugin. Note that there are 2 layouts configured here, books_pagination is used for the paginated collection page, and book is used for the individual book page.

In addition to Jekyll's default page data, you can also use the additional page data and page's paginator data generated by the plugin in the layout:

Paginated collection page

Use page to access the fields below.

Field Description
title Current page's title.
collection Current page's collection.
collection_type Current page's collection type.
page_num_zero_index Current page's 0-index page number.
page_num_one_index Current page's 1-index page number.
page_num_label Current page's page number label.
full_url Current page full url. Can be used to match the current window url for highlighting purpose.

Paginated collection paginator

Use page.paginator to acceess the fields below.

Field Description
total_pages Total number of pages of the paginated collection.
pages All the pages in the collection.
page_num_zero_index Current page's 0-index page number.
page_num_one_index Current page's 1-index page number.
page_num_label Current page's page number label.
collection Current page's collection.
collection_type Current page's collection type.
current_page Current page.
current_page_path Current page full url. Can be used to match the current window url for highlighting purpose.
previous_page Previous page.
previous_page_path Previous page full url. Can be used to generate navigation link.
next_page Next page.
next_page_path Next page full url. Can be used to generate navigation link.

An example of the paginated collection page layout books_pagination:

---
layout: default
---
<h1>Books</h1>
<div class="collection">
{% raw %}
{% for book in page.paginator.collection %}
  <div class="item">
    <div class="title">
      <span>{{ book.title }}</span>
    </div>
    <p>{{ book.excerpt }}</p>
  </div>
{% endfor %}
</div>

{% if page.paginator.total_pages > 1 %}
<div class="pagination">
  <ul class="pager">
    {% if page.paginator.previous_page %}
    <li class="previous">
      <a href="{{ page.paginator.previous_page_path }}"><i class="fa-solid fa-arrow-left"></i> Newer {{ page.paginator.collection_type | capitalize }}</a>
    </li>
    {% endif %}
    {% if page.paginator.next_page %}
    <li class="next">
      <a href="{{ page.paginator.next_page_path }}">Older {{ page.paginator.collection_type | capitalize }} <i class="fa-solid fa-arrow-right"></i></a>
    </li>
    {% endif %}
  </ul>
</div>
{% endif %}
{% endraw %}

The resulting index pages using the example configuration and layout are as such:

Root page

Generated at: _site/books.html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Books</title>
  </head>
  <body>
    <div class="container">
      <h1>Books</h1>
      <div class="collection">
        <div class="item">
          <div class="title">
            <span>Harry Potter 7</span>
          </div>
          <p>This is the final book in the Harry Potter series.</p>
        </div>

        <div class="item">
          <div class="title">
            <span>Harry Potter 6</span>
          </div>
          <p>This is the sixth book in the Harry Potter series.</p>
        </div>

        <div class="item">
          <div class="title">
            <span>Harry Potter 5</span>
          </div>
          <p>This is the fifth book in the Harry Potter series.</p>
        </div>
      </div>

      <div class="pagination">
        <ul class="pager">
          <li class="next">
            <a href="/books/2/index.html">Older Books <i class="fa-solid fa-arrow-right"></i></a>
          </li>
        </ul>
      </div>
    </div>
  </body>
</html>

Next page

Generated at: _site/books/2/index.html.

Note: Header elements omitted for clarity.

<div class="container">
  <h1>Books</h1>
  <div class="collection">
    <div class="item">
      <div class="title">
        <span>Harry Potter 4</span>
      </div>
      <p>This is the fourth book in the Harry Potter series.</p>
    </div>

    <div class="item">
      <div class="title">
        <span>Harry Potter 3</span>
      </div>
      <p>This is the third book in the Harry Potter series.</p>
    </div>

    <div class="item">
      <div class="title">
        <span>Harry Potter 2</span>
      </div>
      <p>This is the second book in the Harry Potter series.</p>
    </div>
  </div>

  <div class="pagination">
    <ul class="pager">
      <li class="previous">
        <a href="/books.html"><i class="fa-solid fa-arrow-left"></i> Newer Books</a>
      </li>
      <li class="next">
        <a href="/books/3/index.html">Older Books <i class="fa-solid fa-arrow-right"></i></a>
      </li>
    </ul>
  </div>
</div>

Last page

Generated at: _site/books/3/index.html.

Note: Header elements omitted for clarity.

<div class="container">
  <h1>Books</h1>
  <div class="collection">
    <div class="item">
      <div class="title">
        <span>Harry Potter 1</span>
      </div>
      <p>This is the first book in the Harry Potter series.</p>
    </div>
  </div>

  <div class="pagination">
    <ul class="pager">
      <li class="previous">
        <a href="/books/2/index.html"><i class="fa-solid fa-arrow-left"></i> Newer Books</a>
      </li>
    </ul>
  </div>
</div>

Individual page paginator

Use page.paginator to access the fields below.

Field Description
collection_type Current page's collection type.
current_page Current page.
current_page_path Current page full url. Can be used to match the current window url for highlighting purpose.
previous_page Previous page.
previous_page_path Previous page full url. Can be used to generate navigation link.
next_page Next page.
next_page_path Next page full url. Can be used to generate navigation link.

An example of the individual page layout book:

---
layout: default
---
<h1>{{ page.title }}</h1>
{% raw %}
<div class="title">
  <span>{{ page.title }}</span>
</div>
<p>{{ content }}</p>

{% if page.paginator %}
<div class="pagination">
  <ul class="pager">
    {% if page.paginator.previous_page %}
    <li class="previous">
      <a href="{{ page.paginator.previous_page_path }}"><i class="fa-solid fa-arrow-left"></i> Previous Book</a>
    </li>
    {% endif %}
    {% if page.paginator.next_page %}
    <li class="next">
      <a href="{{ page.paginator.next_page_path }}">Next Book <i class="fa-solid fa-arrow-right"></i></a>
    </li>
    {% endif %}
  </ul>
</div>
{% endif %}
{% endraw %}

The resulting page using the example configuration and layout are as such:

Individual page

Generated at: _site/books/book-1.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Books</title>
  </head>
  <body>
    <div class="container">
      <h1>Book 1</h1>
      <div class="title">
        <span>Book 1</span>
      </div>
      <p>This is book 1.</p>

      <div class="pagination">
        <ul class="pager">
          <li class="next">
            <a href="/books/book-2.html">Next Book <i class="fa-solid fa-arrow-right"></i></a>
          </li>
        </ul>
      </div>
    </div>
  </body>
</html>

Contributing

Contribution to the gem is very much welcome!

  1. Fork it (https://github.com/coffeebrewapps/coffeebrew_jekyll_paginate/fork).
  2. Create your feature branch (git checkout -b my-new-feature).
  3. Make sure you have setup the repo correctly so that you can run RSpec and Rubocop on your changes. Read more under the Development section.
  4. Commit your changes (git commit -am 'Add some feature').
  5. If you have added something that is worth mentioning in the README, please also update the README.md accordingly and commit the changes.
  6. Push to the branch (git push origin my-new-feature).
  7. Create a new Pull Request.

The repo owner will try to respond to a new PR as soon as possible.

Development

We want to provide a robust gem as much as possible for the users, so writing test cases will be required for any new feature.

If you are contributing to the gem, please make sure you have setup your development environment correctly so that RSpec and Rubocop can run properly.

  1. After forking the repo, go into the repo directory (cd coffeebrew_jekyll_paginate/).
  2. Make sure you have the correct Ruby version installed. This gem requires Ruby >= 2.7.0.
  3. Once you have the correct Ruby version, install the development dependencies (bundle install).
  4. To test that you have everything installed correctly, run the test cases (bundle exec rspec).
  5. You should see all test cases pass successfully.

Source directory structure

All the gem logic lives in the /lib directory:

lib
├── coffeebrew_jekyll_paginate
│   ├── config.yml
│   ├── generator.rb
│   ├── individual_paginator.rb
│   ├── page.rb
│   ├── page_drop.rb
│   ├── paginator.rb
│   ├── validator.rb
│   └── version.rb
└── coffeebrew_jekyll_paginate.rb

The files that are currently in the repo:

File Description
lib/coffeebrew_jekyll_paginate/config.yml This contains the default configuration for the plugin to generate pagination.
lib/coffeebrew_jekyll_paginate/generator.rb This is the generator that reads the configuration and generate pagination.
lib/coffeebrew_jekyll_paginate/individual_paginator.rb This is the abstract model containing the pagination methods for individual pages.
lib/coffeebrew_jekyll_paginate/page.rb This is the abstract model of the paginated collection pages.
lib/coffeebrew_jekyll_paginate/page_drop.rb This is the page drop used by the paginator class so its methods can be used in Liquid template.
lib/coffeebrew_jekyll_paginate/paginator.rb This is the abstract model containing the pagination methods for paginated collection pages.
lib/coffeebrew_jekyll_paginate/validator.rb This validates the configuration.
lib/coffeebrew_jekyll_paginate/version.rb This contains the version number of the gem.
lib/coffeebrew_jekyll_paginate.rb This is the entry point of the gem, and it loads the dependencies.

Test cases directory structure

All the test cases and fixtures live in the /spec directory:

Note: Some files have been omitted for clarity.

spec
├── coffeebrew_jekyll_paginate_spec.rb
├── dest
├── fixtures
│   ├── _books
│   │   ├── 1997-06-26-harry-potter-1.md
│   │   ├── 1998-07-02-harry-potter-2.md
│   │   ├── 1999-07-08-harry-potter-3.md
│   │   ├── 2000-07-08-harry-potter-4.md
│   │   ├── 2003-06-21-harry-potter-5.md
│   │   ├── 2005-07-16-harry-potter-6.md
│   │   └── 2007-07-21-harry-potter-7.md
│   ├── _layouts
│   │   ├── book.html
│   │   ├── default.html
│   │   ├── paginated_books.html
│   │   ├── paginated_posts.html
│   │   └── post.html
│   ├── _posts
│   │   ├── 2021-03-12-test-post-1.md
│   │   ├── 2021-03-28-test-post-2.md
│   │   ├── 2021-05-03-test-post-3.md
│   │   ├── 2021-05-03-test-post-4.md
│   │   ├── 2022-01-27-test-post-5.md
│   │   ├── 2022-03-12-test-post-6.md
│   │   ├── 2022-11-23-test-post-7.md
│   │   └── 2023-02-21-test-post-8.md
│   └── _config.yml
├── scenarios
│   ├── default
│   │   ├── _site
│   │   │   ├── books
│   │   │   │   ├── 1
│   │   │   │   │   └── index.html
│   │   │   │   ├── 2
│   │   │   │   │   └── index.html
│   │   │   │   ├── 1997-06-26-harry-potter-1.html
│   │   │   │   ├── 1998-07-02-harry-potter-2.html
│   │   │   │   ├── 1999-07-08-harry-potter-3.html
│   │   │   │   ├── 2000-07-08-harry-potter-4.html
│   │   │   │   ├── 2003-06-21-harry-potter-5.html
│   │   │   │   ├── 2005-07-16-harry-potter-6.html
│   │   │   │   └── 2007-07-21-harry-potter-7.html
│   │   │   └── posts
│   │   │       ├── 1
│   │   │       │   └── index.html
│   │   │       ├── 2
│   │   │       │   └── index.html
│   │   │       ├── 2021-03-12-test-post-1.html
│   │   │       ├── 2021-03-28-test-post-2.html
│   │   │       ├── 2021-05-03-test-post-3.html
│   │   │       ├── 2021-05-03-test-post-4.html
│   │   │       ├── 2022-01-27-test-post-5.html
│   │   │       ├── 2022-03-12-test-post-6.html
│   │   │       ├── 2022-11-23-test-post-7.html
│   │   │       └── 2023-02-21-test-post-8.html
│   │   └── context.rb
│   └── invalid_config_keys
│       └── context.rb
└── spec_helper.rb

The files that are currently in the repo:

File Description
spec/coffeebrew_jekyll_paginate_spec.rb This is the main RSpec file to be executed. It contains all the contexts of various scenarios.
spec/dest/ This directory is where generated files are located. It will be deleted immediately after each context is executed.
spec/fixtures/ This directory follows the Jekyll site source structure and contains the minimal files required to generate the paginated pages.
spec/fixtures/_books This directory contains the test books, you can add more to it to test your new feature.
spec/fixtures/_posts This directory contains the test posts, you can add more to it to test your new feature.
spec/scenarios/ This directory contains the expected files of various test scenarios.
spec/scenarios/<scenario>/ This is the scenario name.
spec/scenarios/<scenario>/_site/ This directory contains the expected paginated pages.
spec/scenarios/<scenario>/context.rb This is the file that sets up the context for the test case.
spec/spec_helper.rb This contains RSpec configuration and certain convenience methods for the main RSpec file.

Writing test cases

There is a certain convention to follow when writing new test scenarios. The recommendation is to use the rake tasks provided in the gem to generate the scenario files.

For success scenarios, run:

bundle exec rake coffeebrew:jekyll:paginate:test:create_success[test_scenario]

This will generate a placeholder file and directory:

spec
├── coffeebrew_jekyll_paginate_spec.rb
├── scenarios
│   └── test_scenario
│       ├── _site
│       └── context.rb
└── spec_helper.rb

Where the context.rb file will be pre-populated:

# frozen_string_literal: true

CONTEXT_TEST_SCENARIO = "when using test_scenario config"

RSpec.shared_context CONTEXT_TEST_SCENARIO do
  let(:scenario) { "test_scenario" }
  let(:overrides) {} # TODO: remove if unused
  let(:generated_files) {} # TODO: remove if unused
  let(:expected_files) do
    [
    ]
  end
end

For failure scenarios, run:

bundle exec rake coffeebrew:jekyll:paginate:test:create_failure[test_scenario]

This will generate a placeholder file and directory:

spec
├── coffeebrew_jekyll_paginate_spec.rb
├── scenarios
│   └── test_scenario
│       └── context.rb
└── spec_helper.rb

Where the context.rb file will be pre-populated:

# frozen_string_literal: true

CONTEXT_TEST_SCENARIO = "when using test_scenario config"

RSpec.shared_context CONTEXT_TEST_SCENARIO do
  let(:scenario) { "test_scenario" }
  let(:generated_files) { [] }
  let(:expected_files) { [] }
  let(:overrides) do
    {
    }
  end

  let(:expected_errors) do
    [
    ]
  end
end

If you do write other test cases that are not asserting the generated files like above, you can initiate your convention. The repo owner will evaluate the PR and accept the convention if it fits the repo existing convention, or will recommend an alternative if it doesn't.

License

See the LICENSE file.