Laravel applications rarely have rendering problems. What they do have is URL multiplication: one listing route can produce thousands of crawlable variants through pagination, sorting, filtering and tracking parameters. Canonicalisation is how you keep that under control.
A canonical helper you can trust
Do not build the canonical from request()->fullUrl() — that bakes every stray parameter into the tag. Derive it from the route:
// app/Support/Seo.php
public static function canonical(?array $keep = ['page']): string
{
$base = url()->current(); // no query string
$query = collect(request()->query())
->only($keep ?? [])
->filter(fn ($v) => $v !== '' && $v !== '1')
->sortKeys()
->all();
return $query ? $base . '?' . http_build_query($query) : $base;
}
Three behaviours matter here: the base URL has no parameters, only an explicit allowlist survives, and page=1 is dropped so the first page and the bare listing URL do not compete. Output it once in the layout so no template can forget it.
Pagination
Laravel's paginator produces ?page=N by default. Handle it like this:
- Self-canonical every page.
/products?page=3canonicalises to itself. - Keep page 1 clean.
/products?page=1should redirect to or canonicalise to/products, never both existing as indexable. - Make the links crawlable anchors. Buttons that fire JavaScript hide the deeper pages from discovery.
- Differentiate titles — appending "– Page 3" prevents identical titles across dozens of URLs.
- Do not noindex deep pages. A noindexed page still passes links initially, but Google eventually treats long-term noindex as nofollow, orphaning the items only listed there.
- Cap infinite scroll with real paginated URLs behind it so every item is reachable without JavaScript.
Filters and sorting
Not every filter deserves a URL. Classify them:
| Parameter type | Example | Treatment |
|---|---|---|
| Presentation only | ?sort=price_desc, ?view=grid | Canonical to the clean URL; do not link with crawlable anchors |
| Tracking | ?utm_source=, ?gclid= | Canonical to the clean URL; never in sitemaps or internal links |
| Content-defining, low demand | ?colour=teal&size=xl | Canonical to the clean URL; block deep combinations from crawling |
| Content-defining, real demand | "waterproof walking boots" | Promote to a clean indexable route with unique copy |
The last row is the one that earns traffic. If a filtered state matches how people search, give it a static route such as /boots/waterproof, its own heading, its own description, and a place in the sitemap — rather than leaving it as a parameter Google is told to ignore.
Route-model binding and slug changes
When a slug changes, the old URL should redirect rather than 404. Keep slug history and resolve it in the binding:
public function show(string $slug)
{
$product = Product::where('slug', $slug)->first();
if (! $product) {
$historic = SlugHistory::where('slug', $slug)->firstOrFail();
return redirect()->route('products.show', $historic->product->slug, 301);
}
return view('products.show', compact('product'));
}
This one pattern prevents most of the 404 accumulation that catalogues suffer over years of editorial changes — see fixing 404 errors for the wider workflow.
Verification checklist
- Crawl the application and group URLs by canonical value; no cluster should be unexpectedly large.
- Confirm no page canonicalises to the homepage unless it is the homepage.
- Check
?page=1, uppercase paths, and trailing-slash variants resolve to a single form. - Confirm paginated pages self-canonicalise and are reachable as plain anchors.
- Check the sitemap contains only canonical URLs.
- Inspect a filtered URL in Search Console and confirm the Google-selected canonical matches your declared one.