A Jekyll plugin to generate site pagination for collections.
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
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.
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:
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 %}
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. |
If the config overrides have invalid structure, keys or values, the plugin will raise a
Jekyll::Errors::InvalidConfigurationError
during build.
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:
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. |
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:
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>
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>
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>
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:
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>
Contribution to the gem is very much welcome!
git checkout -b my-new-feature
).git commit -am 'Add some feature'
).git push origin my-new-feature
).The repo owner will try to respond to a new PR as soon as possible.
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.
cd coffeebrew_jekyll_paginate/
).bundle install
).bundle exec rspec
).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. |
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. |
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.
See the LICENSE file.