illuminatech/url-trailing-slash
Laravel middleware that enforces consistent trailing slashes by redirecting requests to the canonical URL form. Avoid duplicate-content SEO issues and keep links predictable, with flexible per-route configuration for when a trailing slash should or shouldn't appear.
composer require illuminatech/url-trailing-slash
/foo/ to the /foo route, route middleware never runs.Laravel 11+:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
// Add after TrustProxies so scheme/host are correct behind proxies,
// but still before routing.
$middleware->append(\Illuminatech\UrlTrailingSlash\Middleware\TrailingSlash::class);
})
Laravel 10 and below:
// app/Http/Kernel.php
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\Illuminatech\UrlTrailingSlash\Middleware\TrailingSlash::class,
// ...
];
php artisan config:clear
php artisan route:clear
/blog/my-post/ → /blog/my-post with a permanent redirect, while leaving APIs, webhooks, and admin tooling untouched. Verify with:curl -I http://my-app.test/blog/my-post/
curl -I http://my-app.test/blog/my-post
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminatech\UrlTrailingSlash\Middleware\TrailingSlash;
class CanonicalTrailingSlash
{
public function handle(Request $request, Closure $next)
{
if ($request->is('api/*', 'webhooks/*', 'horizon/*', 'telescope/*', '_ignition/*')) {
return $next($request);
}
// Do not turn POST/PUT/PATCH/DELETE into GET via a 301.
if (! $request->isMethodCacheable()) {
return $next($request);
}
return app(TrailingSlash::class)->handle($request, $next);
}
}
Register CanonicalTrailingSlash globally instead of the package middleware directly when you need exclusions.
Choose one canonicalization layer. If nginx/Apache already redirects trailing slashes, disable one side. Running both server-level and Laravel-level rules is a common source of redirect loops, especially when the proxy and app disagree about scheme/host.
Keep route definitions simple. Define routes in Laravel’s normal style and let the middleware redirect inbound URLs to the canonical form. Avoid mixing /foo and /foo/ route definitions for the same resource.
Treat API and webhook routes as non-redirecting. API clients often do not follow 301/302 redirects, and webhook providers may sign the exact URL. Return consistent 404s or document the required style instead of redirecting.
Be careful with generated URLs. The package primarily normalizes incoming requests. Check whether your version also affects route()/url() output. If it does not, centralize link generation so generated URLs already match the canonical style, especially for pagination, sitemaps, emails, and signed URLs.
Exclude signed URLs unless you control generation. Redirecting a signed URL can invalidate the signature or surprise clients. Generate signed URLs in the canonical form and test them through the middleware.
Use feature tests as policy guards. Add tests for both directions and for exceptions:
$this->get('/about/')->assertRedirect('/about');
$this->get('/about')->assertOk();
$this->get('/api/things/')->assertNotFound(); // or expected API behavior
TrustProxies but before route resolution. If redirects use http:// behind an HTTPS proxy, fix TrustProxies/APP_URL first; otherwise you can create loops.curl -I --max-redirs 0 during rollout. Changing the policy later will not fix clients that cached the old permanent redirect.POST can become a GET and drop the request body. Exclude APIs, form submissions, and webhooks, or ensure the package uses an appropriate status code such as 308./ should stay /; avoid generating // or redirecting the homepage in a loop./search/?q=foo redirects to /search?q=foo (or the reverse) without losing the query string./foo//bar, %2F, and mixed encodings may not be covered by a simple trailing-slash check. Normalize at the web server or with a dedicated URL-normalization middleware if these matter.php artisan config:clear, route:clear, and restart Octane workers if applicable.NOASSERTION; verify the actual license and maintenance status. Because the package is small, pin the version and be prepared to fork or vendor the middleware if needed.How can I help you explore Laravel packages today?