Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Paginateroute Laravel Package

michaloravec/laravel-paginateroute

Adds a paginate route macro to Laravel for pagination via custom routes instead of query strings, with translatable "page" segments, normal/dash/simple URL modes, config publishing, and its own URL helpers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register in three moves. The package auto-registers its service provider, so composer require michaloravec/laravel-paginateroute is the entire install. Then publish the config if you want to change URL shape, and register the macros in App\Providers\RouteServiceProvider::boot():

use PaginateRoute;

public function boot()
{
    PaginateRoute::registerMacros();
    parent::boot();
}

The registerMacros() call must run before your routes are loaded and before parent::boot() — treat it as the first line of boot().

First use case: replace a ?page=N listing route. Where you would normally write:

Route::get('users', 'UsersController@index')->name('users');

write:

Route::paginate('users', 'UsersController@index');

The macro registers /users and /users/page/{page} in one call. Your controller code is unchanged — keep using User::paginate(15) or User::simplePaginate(15). The only thing you must change is the view, because Laravel's built-in $paginator->links() still emits query-string URLs. Swap those for the package's helpers:

@if (PaginateRoute::hasPreviousPage())
  <a href="{{ PaginateRoute::previousPageUrl() }}">Previous</a>
@endif

@if (PaginateRoute::hasNextPage($users))
  <a href="{{ PaginateRoute::nextPageUrl($users) }}">Next</a>
@endif

Note the asymmetry that will trip you up on day one: previousPage() / previousPageUrl() / hasPreviousPage() need no argument (they read the current request), while nextPage(), nextPageUrl(), and hasNextPage() require the paginator. There is no hasNextPage() without it because the current request alone cannot tell you if more records exist.

Where to look first in the repo: config/paginateroute.php (the mode key) and the service provider. The codebase is small enough to read end-to-end in a sitting, and you should — this is a 17-star, single-maintainer package. Pin the version in composer.json and, if the pagination URL is part of your SEO contract, consider vendoring the macro into app/ so a broken release cannot take down your listing pages.

Implementation Patterns

Choose your URL mode deliberately, then stop thinking about it. The config mode key accepts three values, all producing a two-route pair:

Mode Page 1 Page 2
normal /news /news/page/2
dash /news /news/page-2
simple /news /news/2

simple is the nicest-looking but the riskiest: /news/2 collides with any future /news/{slug} route or a numeric ID segment. Use normal or dash for content sites; reserve simple for routes whose entire path space is pagination.

Localization is the real reason to adopt this. Publish the lang files (--tag="lang") and translate the "page" segment per locale, then wrap Route::paginate() in your existing locale route group:

Route::group(['prefix' => '{locale}'], function () {
    Route::paginate('news', 'NewsController@index');
});

You get /en/news/page/2 and /cs/novinky/stranka/2 from the same controller. Because the segment is part of the localized URL contract, a changed translation is a breaking URL change — decide early whether you will redirect old pagination paths or accept the SEO hit. Lock translations down once indexed.

Render the full page list only when you have a LengthAwarePaginator. For numbered pagination, use renderPageList():

{!! PaginateRoute::renderPageList($articles, false, 'pagination', true) !!}

The signature is renderPageList($paginator, $full = false, $class = null, $additionalLinks = false). The $additionalLinks flag adds the « / » first/last arrows shown in the README example. If you need custom markup, use allUrls($paginator, $full) and build the <ul> yourself — it returns a flat array of URLs you can pair with page numbers.

Wire up SEO links from the controller or a Blade component. renderRelLinks() emits HTML5 rel="prev" / rel="next" tags:

<head>
  {!! PaginateRoute::renderRelLinks($articles, true) !!}
</head>

Pass $full = true to get absolute URLs — crawlers expect fully qualified rel links, so use true here even though the default is relative.

Handle the page-1 canonical question explicitly. By default pageUrl(1) returns /news (not /news/page/1), which is what you want for canonicalization. But previousPageUrl($full = true) and addPageQuery($url, $page, $full = true) will happily emit /news/page/1. Audit every place you pass $full = true and confirm /page/1 never appears in a canonical tag or sitemap.

Redirect legacy query-string traffic. This is the migration step the README omits. If /news?page=3 was indexed, add a middleware or route-level redirect to /news/page/3:

if ($request->has('page') && $request->is('news')) {
    return redirect()->to(PaginateRoute::addPageQuery(url('/news'), $request->page), 301);
}

Without this you will serve the same content at two URLs and split link equity.

Cross-route page links use addPageQuery(). When a sidebar "related articles" widget needs a paginated URL for a different route than the current one, pageUrl() won't help — it resolves against the current route. Use addPageQuery($url, $page, $full) to attach the segment to an arbitrary base URL.

Gotchas and Tips

simplePaginate() breaks half the API. allUrls(), renderPageList(), and renderRelLinks() all type-hint LengthAwarePaginator. Call them with a simplePaginate() result and you get a TypeError. If you use simplePaginate() — a reasonable choice for large tables — you are limited to the prev/next helpers. Pick one paginator style per route and be consistent.

Out-of-range pages return 200 with an empty collection. /news/page/9999 does not 404 out of the box, which creates infinitely many crawlable thin-content URLs. Add a guard:

if ($users->isEmpty() && $users->currentPage() > 1) {
    abort(404);
}

Also consider a <meta name="robots" content="noindex"> on pages past a sane depth.

$paginator->links() still emits query strings — this is the #1 adoption bug. Nothing in this package changes Laravel's own paginator view. You must replace every {{ $items->links() }} with the package helpers, or override the paginator view (Paginator::defaultView()) to route through PaginateRoute::pageUrl(). Grep your views for ->links() after installing.

Watch for a macro name collision with Spatie. This package is a fork-in-spirit of spatie/laravel-paginateroute and both register a paginate macro on the Route facade. If both are installed, whichever registers second wins (or throws, depending on Laravel version). Remove one; do not "just try it."

Route caching is fine, but ordering is not. php artisan route:cache works because the macro expands at definition time, not at request time. However, Route::paginate('news', ...) defines /news/page/{page} — a route with a catch-all-shaped segment. Register it before any Route::get('news/{slug}', ...) or the slug route will swallow /news/page/2. This is the most likely runtime failure after deployment.

registerMacros() must survive config cache. Since mode lives in a published config file, php artisan config:cache locks it. Changing mode after a page has been indexed changes your URL scheme site-wide — treat it as a deploy-time decision requiring redirects, not a tweak. Always run config:clear locally after editing.

The package has 0 dependents and one maintainer. The 2024 release fixed a PHP 8.1 str_replace(null) deprecation — meaning it sat unreleased across the 8.0→8.1 transition. Practical posture: pin an exact version ("michaloravec/laravel-paginateroute": "1.0.2"), vendor a copy of the macro into your app if pagination is on a revenue-adjacent surface, and write your own feature tests for /news/page/0, /news/page/abc, /news/page/1, and out-of-range — the package's own suite is described as "integration/smoke tests" and won't cover your route table.

page=0 and non-numeric segments fall through to Laravel's route matching or the macro's own coercion. Verify what /news/page/0 and /news/page/abc actually return in your app before launch; a 500 on a user-editable URL is a cheap win for an attacker.

Debugging tip: when a pagination URL looks wrong, dump PaginateRoute::pageUrl(1), PaginateRoute::pageUrl(2), and PaginateRoute::previousPageUrl() side by side in a test — the relative/absolute and page-1 special-casing differences surface immediately, and they are the source of nearly every "why is my canonical wrong" bug with this package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk