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"
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.
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!
- Fork it (https://github.com/coffeebrewapps/coffeebrew_jekyll_paginate/fork).
- Create your feature branch (
git checkout -b my-new-feature
). - 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.
- Commit your changes (
git commit -am 'Add some feature'
). - If you have added something that is worth mentioning in the README, please also update the README.md accordingly and commit the changes.
- Push to the branch (
git push origin my-new-feature
). - 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.
- After forking the repo, go into the repo directory (
cd coffeebrew_jekyll_paginate/
). - Make sure you have the correct Ruby version installed. This gem requires Ruby >= 2.7.0.
- Once you have the correct Ruby version, install the development dependencies (
bundle install
). - To test that you have everything installed correctly, run the test cases (
bundle exec rspec
). - 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:
CONTEXT_TEST_SCENARIO = "when using test_scenario config"
RSpec.shared_context CONTEXT_TEST_SCENARIO do
let(:scenario) { "test_scenario" }
let(:overrides) {}
let(:generated_files) {}
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:
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.