Reader Stacks

Appending URL Query Parameters to Laravel Pagination Links

By default Laravel pagination links drop your filter/sort query params on page 2+. Here is how to keep them with appends() or withQueryString().

A common bug: a filtered or sorted list works fine on page 1, but clicking "page 2" silently drops the filter — because Laravel's paginator only carries its own page parameter in the links it generates, not the rest of the query string, unless you tell it to.

The problem

// URL: /products?category=tools&sort=price
$products = Product::where('category', request('category'))
    ->paginate(20);

The pagination links render as /products?page=2category and sort are gone, and page 2 shows the unfiltered list.

Fix 1: withQueryString() — carry everything

{{ $products->withQueryString()->links() }}

This appends every current query parameter to every pagination link automatically. It's the right default for most filtered listing pages. Pagination does not automatically preserve arbitrary request query parameters; call withQueryString() (or appends()) explicitly when those parameters need to survive into generated links.

Fix 2: appends() — carry specific parameters only

{{ $products->appends([
    'category' => request('category'),
    'sort' => request('sort'),
])->links() }}

Use this when you want control over exactly which parameters survive pagination — for example, deliberately dropping a one-time "just added" flag while keeping the real filters.

Reading params back on the next page

Carrying the query string in the links only helps if the controller also reads it back on every request, not just the first one:

$products = Product::query()
    ->when(request('category'), fn ($q, $category) => $q->where('category', $category))
    ->when(request('sort'), fn ($q, $sort) => $q->orderBy($sort))
    ->paginate(20)
    ->withQueryString();

The when() clauses make the filters conditional so an empty request still returns the full unfiltered list, and each subsequent page request re-applies the same filters from its own query string.

Put withQueryString() on the paginator, not only in Blade

The paginator is the object that builds the URLs, so attaching the query string before passing it to the view keeps that behavior close to the query and avoids duplicating it in multiple templates:

$products = Product::query()
    ->when($request->filled('category'), function ($query) use ($request) {
        $query->where('category', $request->string('category'));
    })
    ->orderBy('name')
    ->paginate(20)
    ->withQueryString();

return view('products.index', compact('products'));

Calling it in Blade is valid; putting it here is simply harder to forget when the same paginator is rendered by more than one view.

Do not carry the current page parameter back into itself

withQueryString() preserves the request query string while the paginator still controls its own page parameter. With appends(), avoid manually appending page; the paginator needs to replace that value for each generated link. The same rule applies to cursor pagination — let the paginator own its navigation parameter and preserve only the filters and sort state around it.

Validate sort parameters before using them as column names

The filter values in the example are data values and can be bound normally. A sort column is different: SQL identifiers cannot be parameter-bound the same way values can. Do not pass an arbitrary ?sort=... value straight into orderBy(). Map allowed request values to known columns instead:

$sorts = [
    'price' => 'price',
    'name' => 'name',
    'newest' => 'created_at',
];

$sortColumn = $sorts[$request->query('sort')] ?? 'name';

$products = Product::query()
    ->orderBy($sortColumn)
    ->paginate(20)
    ->withQueryString();

That is a pagination-adjacent gotcha worth fixing at the same time: keeping a malicious or invalid sort parameter across pages only makes a bad query repeat consistently.

Topics: Pagination & Filtering