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 Trailing Slash Laravel Package

gnahotelsolutions/laravel-trailing-slash

Laravel middleware that normalizes trailing slashes on incoming URLs, redirecting requests to your preferred format (with or without a trailing slash) through configurable, SEO-friendly redirects. Easy to install and customize for cleaner, consistent routes.

View on GitHub
Deep Wiki
Context7

Getting Started

gnahotelsolutions/laravel-trailing-slash is a small, single-purpose package: it normalises URLs so that every request either has or lacks a trailing slash, issuing a redirect (or rewriting the path) when it doesn't match your chosen convention. It exists to solve the "/blog and /blog/ are two different URLs to Laravel's router but one page to Google" problem.

Minimal setup:

  1. composer require gnahotelsolutions/laravel-trailing-slash — the package is Laravel-agnostic in style (service provider + middleware), so no manual provider registration is needed on Laravel 5.5+.
  2. Publish the config if you need to change the default behaviour: php artisan vendor:publish --provider="Gnahotelsolutions\LaravelTrailingSlash\TrailingSlashServiceProvider" (check the tag listed by php artisan vendor:publish in your version; the provider namespace is the reliable way to target it).
  3. Register the middleware on the web group in app/Http/Kernel.php (or bootstrap/app.php on Laravel 11+, via $middleware->web(append: [...])), after session/start-session middleware but before anything that generates URLs or signs them.
  4. Clear caches (php artisan route:clear && php artisan config:clear) and hit a page both with and without a slash to confirm the redirect direction.

Where to look first:

  • The published config file — it's where the enable flag and the "add vs. strip" direction live. Read it before touching the kernel; most confusion comes from the middleware being active but the config disabling the transform.
  • The middleware class itself (it's a handful of lines). Confirming exactly what status code it issues (301 vs 302) and whether it preserves the query string takes thirty seconds and prevents a lot of guessing.

First real use case: you're adding canonical URLs and rel=canonical tags ahead of an SEO push, and you discover /services/ and /services both return 200 with slightly different content. Install the package, pick "strip trailing slash" (matches Laravel's default route definitions and most Laravel-generated URLs), and you get a single canonical URL without rewriting every Route::get.

Implementation Patterns

Redirect-based normalisation (the default). The middleware intercepts the request, compares $request->getPathInfo() against the configured style, and returns a redirect to the corrected URL. This is the pattern to reach for on public, GET-heavy sites: it's cacheable by CDNs, and it keeps a single canonical form in search indexes. Apply it globally on the web group.

Scoped application. Don't put it on the global stack. Apply it per route group instead, so you can exclude /api, health checks, webhooks, and anything served by a third party that won't follow redirects:

Route::middleware('trailing-slash')->group(function () {
    Route::get('/about', ...);
});

Give the middleware an alias in the kernel and reference the alias, not the FQCN — it makes swapping behaviour in tests trivial.

Route-group prefixing with Route::prefix(). If you serve a section under /docs/, the package's redirect will operate on the full path, including the prefix. Verify that the generated Location header includes the subdirectory when the app is hosted under one (APP_URL with a path); this is the most common misconfiguration.

Paired with canonical tags. Treat the middleware as the enforcement layer and the <link rel="canonical"> as the declaration. Generate canonicals with url()->current() after normalisation, or explicitly rtrim(url()->current(), '/') so the tag never contradicts the redirect.

Integration with signed URLs and pagination. If you use signed routes, generate links after the convention is applied — URL::signedRoute() hashes the path, so a redirect that mutates the path invalidates the signature. For pagination, $paginator->url() builds from the current request path; test page 2 explicitly, because a redirect on /posts/?page=2 that drops the query string silently resets the user to page 1.

Testing pattern. Assert the redirect rather than the final page:

$this->get('/about/')->assertRedirect('/about');
$this->get('/about')->assertOk();

Add one test per HTTP verb you actually serve. assertRedirect also lets you assert the exact Location, which is the cheapest way to catch subdirectory and query-string regressions.

Gotchas and Tips

  • 301s are cached aggressively. Browsers and CDNs will remember a wrong redirect for a long time. While tuning the config, temporarily force a 302 (or test with curl -I on a fresh profile) so a misconfiguration doesn't become sticky.
  • Redirects drop request bodies. A POST to /orders/ that gets redirected to /orders becomes a GET (or a bodyless request) on the client side. Exclude non-GET verbs — either via the config if it supports it, or by gating the middleware to $request->isMethod('GET') in a thin wrapper of your own.
  • Laravel's router genuinely distinguishes the two forms. Route::get('/about') does not match /about/. That means the middleware must run before routing resolves — which it does when registered as normal middleware, but not if you've wired it as a route-level middleware on a route that already failed to match. Symptom: 404s instead of redirects. Fix: apply it on the group, not on individual routes.
  • Exclude API and webhook routes. API clients (Guzzle, fetch in a mobile app) may not follow redirects, and third-party webhook providers often won't. Silent 301s on POST /api/... are painful to debug from the caller's side.
  • Static assets under public/ are unaffected — the middleware only sees requests routed through Laravel. But if you serve a SPA through a catch-all route, the middleware will rewrite client-side routes too; make sure the redirect preserves the fragment (it won't — fragments never reach the server, so the browser handles it, but verify deep-link behaviour).
  • url() and route() helpers are not changed by this package. If your app generates /about/ in Blade today, it will keep generating it; you'll get a redirect on every click. Audit hardcoded trailing slashes in views and JS config, not just server routes.
  • Trailing slash on the root URL. / must be exempt from any "strip" rule, or you'll redirect-loop. Confirm the package short-circuits on / — if you're writing a wrapper, if ($request->getPathInfo() === '/') return $next($request); is mandatory.
  • Middleware ordering matters. Place it after TrustProxies/TrustHosts so the Location header is built from the correct scheme and host, and before session start if you don't want a session cookie set on a request that's about to be redirected. The reverse (session first) is harmless functionally but writes sessions for crawler redirects.
  • Query strings, encoded characters, and case. Assert that ?foo=bar&baz=1 survives the redirect and that percent-encoded paths round-trip. Laravel's getRequestUri() vs getPathInfo() distinction is the usual source of bugs here.
  • Version 1.5.0 shipped with only a changelog link (no documented features, fixes, or deprecations). Treat it as a routine patch: pin the version, run your redirect tests, and re-read the published config after vendor:publish --force in case a new key was added with a default that differs from the old hardcoded behaviour.
  • Extension point: if you need conditional behaviour (e.g. strip slashes for HTML, leave JSON alone), don't fork — subclass or wrap the middleware, check $request->expectsJson() or $request->header('Accept'), and register the wrapper under the same alias. The package's surface area is small enough that a five-line wrapper covers most edge cases.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel