SEO-Friendly Pagination in CodeIgniter 4 Without Rewriting Queries
CodeIgniter 4's Paginator service handles page math and link generation, but you still own the total count, route shape, and view markup. Here is how to wire SEO-friendly pagination with minimal configuration and a publishable theme.
09 Nov 2025, 07:48 UTC

The problem is not pagination, it is duplicate URLs and mismatched markup
You ship a listing page. Page 2 loads, page 3 loads, and search engines see ?page=2, ?page=3 with the same template and no clear canonical signal. At the same time your front-end expects Tailwind or Bootstrap classes, but the default pager markup uses generic classes. The useful takeaway is that CodeIgniter 4 gives you a Paginator service for calculation and URL generation, but you still own the total count, the route shape, and the view markup.
What the Paginator service actually does
CodeIgniter 4 provides a native Paginator service. It handles page number calculation, URL generation, and HTML rendering. It does not run your query. You supply items per page, total items count, and a base URL. The service then builds numeric links and next/previous navigation out of the box.
The service is intentionally thin. Counting rows and fetching the current slice remains your responsibility. That separation keeps the service fast and framework-agnostic, but it means you must wire the total count from your model or query builder.
Make URLs SEO-friendly with routes.php
SEO-friendly URLs need proper route configuration in routes.php. By default CodeIgniter uses query strings like ?page=2. That works, but clean paths such as /blog/page/2 are easier to read and control.
Define a route that captures the page segment and passes it to the controller. The controller then reads the segment and uses it when building links. If you change the URL pattern, update the base URL you pass to the Paginator so generated links stay consistent.
Risk note
Changing route patterns after content is indexed can create 404s. Keep the old pattern working during a transition or set redirects.
Customize the view to match your CSS framework
Pagination views can be published to application/views. The default theme is generic. Publishing lets you replace the markup without editing system files.
Default themes may not match all CSS frameworks without customization. After publishing, edit the view files to use your framework classes for active, disabled, and link states. This is a one-time change per project.
Worked example with a Blog controller
Create a controller from the project root. This requires write permission to app/Controllers.
php spark make:controller Blog
Expected check: a file is created at app/Controllers/Blog.php. Risk: the command overwrites an existing file with the same name.
Example controller logic using the Paginator service. Replace placeholders with your model and route names.
namespace App\Controllers;
use App\Models\BlogModel;
class Blog extends BaseController
{
public function index()
{
$perPage = 10;
$page = (int) $this->request->getGet('page') ?: 1;
$model = new BlogModel();
$total = $model->countAllResults();
$offset = ($page - 1) * $perPage;
$posts = $model->findAll($perPage, $offset);
$pager = service('pager');
$links = $pager->makeLinks($page, $perPage, $total, 'blog');
return view('blog/index', [
'posts' => $posts,
'pager' => $links,
]);
}
}
The view receives $pager and renders it. The controller owns counting and slicing; the service owns link math.
Trade-off and limitation
The library requires manual configuration of total item count from database queries. For large tables a full count can be expensive. Consider caching the count for a short TTL or using an estimated count for very large datasets.
Also, the Paginator does not add rel="next"/rel="prev" or canonical tags automatically. You must add those in your layout if you need them for SEO.
Check it works
Verify query string parameters like ?page=2 are correctly parsed and the offset changes. Inspect the generated HTML structure to confirm it matches expected CSS framework classes. Check that the last page does not generate a link beyond total pages and that next/previous links disable appropriately.
Publish the default theme once, adjust classes, and keep the route and base URL in sync. That gives you minimal configuration pagination with markup you control.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.