Arka Roy

Custom Category Pagination in Jekyll

Jekyll is an awesome project. But how can we paginate only one type of post? Or what about custom permalink?

Jekyll is a blog aware static site generator. When I was working on a blog, I wanted to separate the blog from other sections like projects. Of course I would be using custom post type for the projects if I was developing on Wordpress. Here I thought to stay straight and simple and I just created two site categories: project and blog and I am using custom permalink which makes it difficult to use the default pagination setup. And I also want to paginate only the blog category.

Here we will discuss on how to paginate a specific category in Jekyll with custom plugin built from scratch.

Let's get started.

Site Structure

In order for the plugin to work, I have removed the index.html from blog category (i.e. directory). Now my site structure looks like this:

+ index.html
+ about.html
+ blog <- no index page for blog
    + _posts
        + 2012-01-02-blog-post-01.markdown
        + 2012-01-03-blog-post-02.markdown
+ portfolio
    + index.html <- index page for portfolio
    + _posts 
        + 2012-02-01-project-01.markdown
        + 2012-02-02-project-02.markdown

I want the index.html in the blog category to be generated by the plugin. Here is the site structure I wish to have:

+ index.html
+ about.html
+ blog
    + index.html
    + pages
        + 2
            + index.html
        + 3
            + index.html
        ...
            ...
    + blog-post-01
        + index.html
    + blog-post-02
        + index.html
+ portfolio
    + project-01
        + index.html
    + project-02
        + index.html

As you can see, index.html in the blog category will be generated by the plugin as the very first page of the pagination. And from second page onward, it will live in respective directories under pages directory.

Liquid Template

The template for iterating over the current set of posts and showing them look like this:

{% raw %}
<h1>Blog</h1>

{% for post in page.current_posts %}
<article class="entry">
    <h3 class="entry-title">
        <a href="{{ post.url | prepend: site.baseurl | prepend: site.url }}" title="Read more on {{ post.title }}" rel="bookmark">{{ post.title }}</a>
    </h3>
    <div class="entry-excerpt">
        {{ post.excerpt }}
    </div>
    <div class="entry-meta">
        <em>{{ post.date | date: "%b %-d, %Y" }} by {{ site.author.name }}</em>
    </div>
</article>
{% endfor %}
{% endraw %}

There should also be navigation links for previous and next pages. Here goes the template for that:

{% raw %}
{% if page.total_pages > 1 %}
    <div class="posts-nav-links clearfix">        
        {% if page.prev_page %}
            {% if page.prev_page == 1 %}
                <a class="pull-left" href="/blog/">&lt;</a>
            {% else %}
                <a class="pull-left" href="/blog/pages/{{ page.prev_page }}">&lt;</a>
            {% endif %}
        {% endif %}
        {% if page.next_page %}
            <a class="pull-right" href="/blog/pages/{{ page.next_page }}">&gt;</a>
        {% endif %}
    </div>
{% endif %}
{% endraw %}

I include these templates in a heredoc inside my plugin.

The Configuration

Before diving into writing the plugin, we need to set some config options in the _config.yml file to make the plugin a bit more maintainable.

We need to provide an option to later modify number of posts in a page without touching the plugin. For this reason, we are going to include posts_per_page in our _config.yml.

paginated_category: "blog"
posts_per_page: 20

The setting paginated_category will tell the plugin which category to use for pagination. We may hardcode this in our plugin, but it is a bit more professional to provide an option to change the setting later without looking at the plugin.

The Code

First, create a file CategoryPostPaginator.rb inside your _plugins directory.

The first class to create is the "Generator." All generators are called by Jekyll at site build, so if you want code that's going to create new pages or content, you want to sub-class this class.

When Jekyll calls a generator, it calls the generate method, so that's the first method to implement. In our class, it loops through all the posts in the site and group the posts according to their position (or index). Then it crates a page for each group.

class CategoryPostPaginator < Generator

    def generate(site)
        all_posts = site.categories[site.config['paginated_category']]
        site.data['all_posts'] = all_posts
        posts_per_page = Float(site.config['posts_per_page'])
        total_posts = Float(all_posts.size)
        total_pages = Float(total_posts / posts_per_page)
        total_pages = total_pages.ceil
        site.data['paginated_pages'] = Hash.new
        (1..total_pages).each do |page_num|
            site.pages << PostListingPage.new(site, total_pages, page_num, all_posts.slice!(0, posts_per_page.to_i))
        end
    end

end

Next, we need to subclass the Page class for our specific needs. This code is very specific to the site I was working on, you may want to change the logic here to meet your needs. Basically, I am creating a series of pages to show category indexes. This code customizes the layout that's used and adds some information to the generated page. I have included my liquid template in heredoc statement.

class PostListingPage < Page

    def initialize(site, total_pages, current_page, current_posts)
        @site = site
        @total_pages = total_pages
        @current_page = current_page
        self.ext = '.html'
        self.basename = 'index'
        prev_page = nil
        next_page = current_page + 1
        if current_page > 1
            prev_page = current_page - 1
        end
        if next_page > total_pages
            next_page = nil
        end
        self.data = {
            'layout' => 'default',
            'title' => "Blog",
            'current_posts' => current_posts,
            'total_pages' => total_pages,
            'current_page' => current_page,
            'prev_page' => prev_page,
            'next_page' => next_page
        }
        self.content = <<-EOS
{% raw %}
<h1>Blog</h1>

{% for post in page.current_posts %}
    <article class="entry">
        <h3 class="entry-title">
            <a href="{{ post.url | prepend: site.baseurl | prepend: site.url }}" title="Read more on {{ post.title }}" rel="bookmark">{{ post.title }}</a>
        </h3>
        <div class="entry-excerpt">
            {{ post.excerpt }}
        </div>
        <div class="entry-meta">
            <em>{{ post.date | date: "%b %-d, %Y" }} by {{ site.author.name }}</em>
        </div>
    </article>
{% endfor %}

{% if page.total_pages > 1 %}
    <div class="posts-nav-links clearfix">        
        {% if page.prev_page %}
            {% if page.prev_page == 1 %}
                <a class="pull-left" href="/blog/">&lt;</a>
            {% else %}
                <a class="pull-left" href="/blog/pages/{{ page.prev_page }}">&lt;</a>
            {% endif %}
        {% endif %}
        {% if page.next_page %}
            <a class="pull-right" href="/blog/pages/{{ page.next_page }}">&gt;</a>
        {% endif %}
    </div>
{% endif %}
{% endraw %}
EOS
        
    end

    def url
        if @current_page == 1
            File.join("/", "blog", 'index.html')
        else
            File.join("/", "blog", "pages", "#{@current_page}", 'index.html')
        end
    end

    def to_liquid
        Utils.deep_merge_hashes(self.data, {
            "url" => self.url,
            "content" => self.content
        })
    end

    def html?
        true
    end

end

That is the end of our discussion. Hope you enjoyed it. Just give it a try.

Other Articles

Download ZIP File Dynamically with PHP

We will see how we can make a webpage act as an initializer to download a zip file. We will just provide the location of the file and PHP will download it to the user. In the back-end, the HTTP headers are responsible for the download. We will set the headers with PHP.

Unparse a Parsed URL in PHP

PHP has a nice and very useful method parse_url to parse and split the url into applicable url components and return the list of component as associative array. But sometimes, you may need to reverse the process.