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

backstage/laravel-trailing-slash

A Laravel package for controlling trailing slash behavior on your application's routes, letting you normalize, redirect, or enforce trailing slashes consistently. Keeps URLs canonical, avoids duplicate-content issues, and helps prevent mismatched redirects with simple, configurable middleware.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Read the source before installing. With zero stars and zero dependents, the source is the documentation. Clone it and read in this order:

  • composer.json — supported Laravel/PHP ranges (this is your first go/no-go).
  • src/ — count the classes. Expect a middleware, a service provider, and something that touches UrlGenerator. If the provider rebinds url or UrlGenerator, note exactly where.
  • config/ — the actual config keys and their defaults. Do not trust the README's naming; use the published file.
  • tests/ — if this directory is missing or thin, plan to vendor the code into app/ rather than depend on it.

2. Install and publish the config.

composer require backstage/laravel-trailing-slash
php artisan vendor:publish --provider="Backstage\TrailingSlash\TrailingSlashServiceProvider"

3. Make the product decision first, then set it. Pick one direction — enforce (/blog/) or strip (/blog) — and encode it in one place. Content sites copying WordPress/static-site conventions usually enforce; Laravel-native apps usually strip. Do not enable both behaviors per-group in v1; get one convention working end to end.

4. Register the middleware in the right slot. In bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (10 and below), the middleware must run:

  • after TrustProxies and host/scheme resolution, otherwise redirect targets are built from the wrong host and you create an open-redirect vector.
  • before ValidateSignature, Authenticate, and SubstituteBindings, otherwise signed URLs 403 and route-model binding runs against a URL you're about to redirect away from.

5. First use case: one route group, GET/HEAD only. Do not go global on day one. Pick your public marketing/content routes, enable normalization for that group, and assert in a feature test that /blog and /blog/ resolve to the same 200-or-301 pair. That single test is your proof the thing works at all.


Implementation Patterns

Canonical URL service wrapper. Never let templates call the package directly. Wrap it once so you can swap the implementation when you fork:

// app/Support/CanonicalUrl.php
final class CanonicalUrl
{
    public static function to(string $name, array $params = []): string
    {
        return route($name, $params); // package adjusts route() output
    }
}

Every Blade <a>, <link rel="canonical">, sitemap entry, and pagination link routes through this. When the outbound side breaks after a Laravel upgrade, you fix one file.

Canonical tag must match the generated URL, not the request URL. The classic bug is a canonical tag built from $request->fullUrl() (which reflects the off-policy inbound form) while your links use route() (which reflects the policy). Build both from the same generator:

<link rel="canonical" href="{{ \App\Support\CanonicalUrl::to($routeName, $params) }}">

Staged enforcement. Run the middleware in log-only mode for a week before it issues redirects. Log [canonical] would-redirect {method} {path} and grep for:

  • POST/PUT/PATCH/DELETE paths (these must not be redirected — see gotchas).
  • Paths that appear with both slash forms in the log, which indicates an internal link or a template emitting the wrong form.
  • Signed URLs (?signature=) — the presence of any in the log is a merge blocker.

Route-group scoping for mixed apps. API routes should almost never redirect — clients don't follow 301s gracefully and it adds latency to every mis-shaped call. Apply the middleware only to the web group, or explicitly exclude api/*, health checks, and webhook receivers.

Test the contract, not the package. Write feature tests that survive a fork:

it('redirects the non-canonical form once', function () {
    $this->get('/blog/?page=2')
        ->assertStatus(301)
        ->assertRedirect('/blog?page=2');
});

Cover: status code, query-string preservation, fragment non-preservation (fragments never reach the server), POST behavior, and signed-URL behavior.

Migration/backlink workflow. When moving to an opposite convention, publish a redirect map first, enable redirects second, then monitor Search Console "Page with redirect" vs. soft-404 counts. Never flip direction and remove the old behavior in the same deploy.


Gotchas and Tips

Signed URLs break silently. Laravel signs the full path. A slash rewrite changes the path and invalidates the signature, producing 403s on password reset, email verification, and signed asset links. Verify with a real URL::temporarySignedRoute() round trip before enabling redirects. If the middleware doesn't skip signed requests, add that skip yourself — this is the most likely reason to fork.

301 on POST drops the body. Most HTTP clients downgrade a 301 to GET and discard the payload. If the package isn't method-aware, scope it to GET/HEAD. If you want to normalize non-idempotent requests, the correct code is 308, and it must be a deliberate choice — not a default.

route:cache divergence. If the package hooks route registration rather than the UrlGenerator, php artisan route:cache can bypass it and production will behave differently from local. After any deploy that runs route:cache, hit a known non-canonical URL and confirm the redirect still fires. Same for config:cache if config is read outside the provider's register().

Middleware ordering is load-bearing. Too early: redirect target built from an untrusted Host header (open redirect) or from http:// behind a proxy (forceScheme hasn't applied yet). Too late: signature validation and auth already ran. If you see redirects pointing at the wrong host or scheme, it's ordering, not config.

Redirect loop signature. If a CDN or load balancer strips or adds the slash after the app responds, the app sees its own redirect target as off-policy and loops. Symptoms: ERR_TOO_MANY_REDIRECTS, or a crawl report full of 301 chains longer than one hop. Add a loop guard (compare against the original request path, bail with a 200 or a 404) and check the edge config before touching the package.

Octane and long-lived workers. If the package rebinds the URL generator in a service provider, verify it survives worker reuse. Test with octane:start locally, not just artisan serve.

Two packages patching UrlGenerator = silent override. If you already have localization, tenancy, or CDN-asset URL shaping, confirm only one binding wins and that it's the one you expect. URL::forceScheme() and URL::forceRootUrl() must compose, not conflict.

Root URL edge case. '/' normalized under an enforce policy is '/', not '//'. Verify the package handles it; a redirect to // becomes a scheme-relative URL to a different host.

Edge caches keep serving the old form. After enabling redirects, purge the CDN. Otherwise clients get cached 200s for the non-canonical form while your origin correctly 301s — you'll see "it works with curl but not in the browser."

Where to extend. The clean extension points, in order of preference: (1) a config flag you add yourself to gate the middleware, (2) a wrapper service in app/, (3) a subclass of the middleware registered in its place, (4) a fork under your namespace with attribution. Given the maturity profile, expect to land on (4) within a Laravel major or two — budget for it now by keeping the package behind your own CanonicalUrl service and one middleware registration line.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager