Rails Pagination with Kaminari
Kaminari is a scope and engine-based pagination library that provides a clean, powerful, customizable paginator for Rails applications. It's non-intrusive, chainable with ActiveRecord, and highly customizable.
Quick Setup
# Add to Gemfile
bundle add kaminari
# Generate configuration file (optional)
rails g kaminari:config
# Generate view templates for customization (optional)
rails g kaminari:views default
Basic Usage
Controller Pagination
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.order(:created_at).page(params[:page])
# Returns 25 items per page by default
end
end
View Helper
<!-- app/views/posts/index.html.erb -->
<%= paginate @posts %>
That's it! Kaminari automatically adds pagination links.
Core Methods
Page Scope
# Basic pagination
User.page(1) # First page, 25 items
User.page(params[:page]) # Dynamic page from params
# Custom per-page
User.page(1).per(50) # 50 items per page
# Chaining with scopes
User.active.order(:name).page(params[:page]).per(20)
# With associations
User.includes(:posts).page(params[:page])
Pagination Metadata
users = User.page(2).per(20)
users.current_page #=> 2
users.total_pages #=> 10
users.total_count #=> 200
users.limit_value #=> 20
users.first_page? #=> false
users.last_page? #=> false
users.next_page #=> 3
users.prev_page #=> 1
users.out_of_range? #=> false
Configuration
Global Configuration
# config/initializers/kaminari_config.rb
Kaminari.configure do |config|
config.default_per_page = 25 # Default items per page
config.max_per_page = 100 # Maximum allowed per page
config.max_pages = nil # Maximum pages (nil = unlimited)
config.window = 4 # Inner window size
config.outer_window = 0 # Outer window size
config.left = 0 # Left outer window
config.right = 0 # Right outer window
config.page_method_name = :page # Method name (change if conflicts)
config.param_name = :page # URL parameter name
end
Per-Model Configuration
# app/models/post.rb
class Post < ApplicationRecord
paginates_per 50 # This model shows 50 per page
max_paginates_per 100 # User can't request more than 100
max_pages 100 # Limit to 100 pages total
end
View Helpers
Basic Pagination
<!-- Simple pagination links -->
<%= paginate @posts %>
<!-- With options -->
<%= paginate @posts, window: 2 %>
<%= paginate @posts, outer_window: 1 %>
<%= paginate @posts, left: 1, right: 1 %>
<!-- Custom parameter name -->
<%= paginate @posts, param_name: :pagina %>
<!-- For AJAX/Turbo -->
<%= paginate @posts, remote: true %>
Navigation Links
<!-- Previous/Next links -->
<%= link_to_prev_page @posts, 'Previous', class: 'btn' %>
<%= link_to_next_page @posts, 'Next', class: 'btn' %>
<!-- With custom content -->
<%= link_to_prev_page @posts do %>
<span aria-hidden="true">←</span> Older
<% end %>
<%= link_to_next_page @posts do %>
Newer <span aria-hidden="true">→</span>
<% end %>
Page Info
<!-- Shows: "Displaying posts 1 - 25 of 100 in total" -->
<%= page_entries_info @posts %>
<!-- Custom format -->
<%= page_entries_info @posts, entry_name: 'item' %>
SEO Helpers
<!-- Add rel="next" and rel="prev" link tags to <head> -->
<%= rel_next_prev_link_tags @posts %>
URL Helpers
# Get URLs for navigation
path_to_next_page(@posts) #=> "/posts?page=3"
path_to_prev_page(@posts) #=> "/posts?page=1"
Customization
Generating Custom Views
# Generate default theme
rails g kaminari:views default
# Generate with namespace
rails g kaminari:views default --views-prefix admin
# Generate specific theme
rails g kaminari:views bootstrap4
This creates templates in app/views/kaminari/:
_first_page.html.erb_prev_page.html.erb_page.html.erb_next_page.html.erb_last_page.html.erb_gap.html.erb_paginator.html.erb
Using Themes
<!-- Default theme -->
<%= paginate @posts %>
<!-- Custom theme -->
<%= paginate @posts, theme: 'my_theme' %>
<!-- Bootstrap theme -->
<%= paginate @posts, theme: 'twitter-bootstrap-4' %>
Custom Pagination Template
<!-- app/views/kaminari/_paginator.html.erb -->
<nav class="pagination" role="navigation" aria-label="Pagination">
<ul class="pagination-list">
<%= first_page_tag %>
<%= prev_page_tag %>
<% each_page do |page| %>
<% if page.left_outer? || page.right_outer? || page.inside_window? %>
<%= page_tag page %>
<% elsif !page.was_truncated? %>
<%= gap_tag %>
<% end %>
<% end %>
<%= next_page_tag %>
<%= last_page_tag %>
</ul>
</nav>
API Pagination
JSON Response
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ApplicationController
def index
@posts = Post.page(params[:page]).per(params[:per_page] || 20)
render json: {
posts: @posts.map { |p| PostSerializer.new(p) },
meta: pagination_meta(@posts)
}
end
private
def pagination_meta(collection)
{
current_page: collection.current_page,
next_page: collection.next_page,
prev_page: collection.prev_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end
API Response Helper
# app/controllers/concerns/paginatable.rb
module Paginatable
extend ActiveSupport::Concern
def paginate(collection)
collection
.page(params[:page] || 1)
.per(params[:per_page] || default_per_page)
end
def pagination_links(collection)
{
self: request.original_url,
first: url_for(page: 1),
prev: collection.prev_page ? url_for(page: collection.prev_page) : nil,
next: collection.next_page ? url_for(page: collection.next_page) : nil,
last: url_for(page: collection.total_pages)
}
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count,
per_page: collection.limit_value
}
end
private
def default_per_page
20
end
end
Performance Optimization
Without Count Query
For very large datasets, skip expensive COUNT queries:
# Controller
def index
@posts = Post.order(:created_at).page(params[:page]).without_count
end
# View - use simple navigation only
<%= link_to_prev_page @posts, 'Previous' %>
<%= link_to_next_page @posts, 'Next' %>
Note: total_pages, total_count, and numbered page links won't work with without_count.
Eager Loading
# Prevent N+1 queries
@posts = Post.includes(:user, :comments)
.order(:created_at)
.page(params[:page])
Caching
# Fragment caching
<% cache ["posts-page", @posts.current_page] do %>
<%= render @posts %>
<%= paginate @posts %>
<% end %>
Advanced Features
Paginating Arrays
# Controller
@items = expensive_operation_returning_array
@paginated_items = Kaminari.paginate_array(@items, total_count: @items.count)
.page(params[:page])
.per(10)
# Or with known total
@paginated_items = Kaminari.paginate_array(
@items,
total_count: 145,
limit: 10,
offset: (params[:page].to_i - 1) * 10
).page(params[:page]).per(10)
SEO-Friendly URLs
# config/routes.rb
resources :posts do
get 'page/:page', action: :index, on: :collection
end
#