backstage / laravel-redirects
Add redirects to your Laravel app using a database, so you can dynamically manage it without writing code.
Fund package maintenance!
backstagephp
Installs: 349
Dependents: 2
Suggesters: 0
Security: 0
Stars: 2
Watchers: 2
Forks: 0
Open Issues: 0
pkg:composer/backstage/laravel-redirects
Requires
- php: ^8.3
- backstage/laravel-trailing-slash: ^0.1.0
- illuminate/contracts: ^10.0 || ^11.0 || ^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1 || ^7.10.0
- orchestra/testbench: ^9.0.0 || ^8.22.0
- pestphp/pest: ^4.1
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- dev-main
- v2.0.0-beta43
- v2.0.0-beta42
- v2.0.0-beta41
- v2.0.0-beta40
- v2.0.0-beta39
- v2.0.0-beta38
- v2.0.0beta-37
- v2.0.0-beta36
- v2.0.0-beta35
- v2.0.0-beta34
- v2.0.0-beta33
- v2.0.0-beta32
- v2.0.0-beta31
- v2.0.0-beta30
- v2.0.0-beta29
- v2.0.0-beta28
- v2.0.0-beta27
- v2.0.0-beta26
- v2.0.0-beta25
- v2.0.0-beta24
- v2.0.0-beta23
- v2.0.0-beta22
- v2.0.0-beta21
- v2.0.0-beta20
- v2.0.0-beta19
- v2.0.0-beta18
- v2.0.0-beta17
- v2.0.0-beta16
- v2.0.0-beta15
- v2.0.0-beta14
- v2.0.0-beta13
- v2.0.0-beta12
- v2.0.0-beta11
- v2.0.0-beta10
- v2.0.0-beta9
- v2.0.0-beta8
- v2.0.0-beta7
- v2.0.0-beta6
- v2.0.0-beta5
- v2.0.0-beta4
- v2.0.0-beta3
- v2.0.0-beta2
- v2.0.0-beta1
- v1.1.54
- v1.1.53
- v1.1.52
- v1.1.51
- v1.1.50
- v1.1.49
- v1.1.48
- v1.1.47
- v1.1.46
- v1.1.45
- v1.1.44
- v1.1.43
- v1.1.42
- v1.1.41
- v1.1.40
- v1.1.39
- v1.1.38
- v1.1.37
- v1.1.36
- v1.1.35
- v1.1.34
- v1.1.33
- v1.1.32
- v1.1.31
- v1.1.30
- v1.1.29
- v1.1.28
- v1.1.27
- v1.1.26
- v1.1.25
- v1.1.24
- v1.1.23
- v1.1.22
- v1.1.21
- v1.1.20
- v1.1.19
- v1.1.18
- v1.1.17
- v1.1.16
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.1
- dev-develop
This package is auto-updated.
Last update: 2026-01-29 12:44:54 UTC
README
A powerful and flexible Laravel package for managing HTTP redirects through a database-driven approach. Dynamically create, update, and track redirects without modifying code or redeploying your application.
Features
- Database-driven redirects - Manage redirects dynamically without code changes
- Multiple matching strategies - HTTP, wildcard, and strict matching middleware
- Status code support - 301, 302, 307, 308 redirects
- Query string preservation - Automatically maintains query parameters
- Hit tracking - Monitor redirect usage with built-in analytics
- Event-driven automation - Automatically create redirects when URLs change
- SEO-friendly - Proper HTTP status codes and trailing slash handling
- Case sensitivity control - Configure case-sensitive or insensitive matching
- Trailing slash handling - Flexible trailing slash sensitivity options
- Protocol agnostic - Works with HTTP and HTTPS seamlessly
Table of Contents
- Installation
- Configuration
- Usage
- Middleware
- Advanced Usage
- API Reference
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Installation
You can install the package via Composer:
composer require backstage/laravel-redirects
Publish and run the migrations:
php artisan vendor:publish --tag="laravel-redirects-migrations"
php artisan migrate
This will create a redirects table with the following structure:
| Column | Type | Description |
|---|---|---|
| ulid | ULID | Primary key |
| source | string | The source URL to redirect from |
| destination | string | The destination URL to redirect to |
| code | integer | HTTP status code (301, 302, 307, 308) |
| hits | integer | Number of times this redirect was triggered |
| created_at | timestamp | When the redirect was created |
| updated_at | timestamp | When the redirect was last modified |
Optionally, publish the configuration file:
php artisan vendor:publish --tag="laravel-redirects-config"
Configuration
The configuration file config/redirects.php provides extensive customization options:
return [ /* * Available HTTP status codes for redirection. * Uncomment additional codes as needed. */ 'status_codes' => [ 301 => 'Moved Permanently', // Permanent redirect, cached by browsers 302 => 'Found', // Temporary redirect, not cached 307 => 'Temporary Redirect', // Temporary, maintains HTTP method 308 => 'Permanent Redirect', // Permanent, maintains HTTP method ], /* * The model to use for managing redirects. * Override this to use your own custom model. */ 'model' => Backstage\Redirects\Laravel\Models\Redirect::class, /* * Default status code for new redirects. * Can be overridden via REDIRECT_DEFAULT_STATUS_CODE env variable. */ 'default_status_code' => env('REDIRECT_DEFAULT_STATUS_CODE', 301), /* * Case sensitivity for URL matching. * * false: /example and /Example are treated as the same * true: /example and /Example are treated as different URLs */ 'case_sensitive' => env('REDIRECT_CASE_SENSITIVE', false), /* * Trailing slash sensitivity for URL matching. * * false: /example and /example/ are treated as the same * true: /example and /example/ are treated as different URLs */ 'trailing_slash_sensitive' => env('REDIRECT_TRAILING_SLASH_SENSITIVE', false), /* * Add trailing slash to redirect destinations. * Useful for maintaining URL consistency and SEO. */ 'trailing_slash' => env('REDIRECT_WITH_TRAILING_SLASH', false), /* * Middleware stack for handling redirects. * Order matters - first match wins. * * - HttpRedirects: Matches URLs with protocol/www variations * - WildRedirects: Partial URL matching (contains) * - StrictRedirects: Exact URL matching */ 'middleware' => [ Backstage\Redirects\Laravel\Http\Middleware\HttpRedirects::class, Backstage\Redirects\Laravel\Http\Middleware\WildRedirects::class, Backstage\Redirects\Laravel\Http\Middleware\StrictRedirects::class, ], ];
Environment Variables
Add these to your .env file for environment-specific configuration:
REDIRECT_DEFAULT_STATUS_CODE=301 REDIRECT_CASE_SENSITIVE=false REDIRECT_TRAILING_SLASH_SENSITIVE=false REDIRECT_WITH_TRAILING_SLASH=false
Usage
Creating Redirects
Database Seeder
use Backstage\Redirects\Laravel\Models\Redirect; Redirect::create([ 'source' => '/old-page', 'destination' => '/new-page', 'code' => 301, ]);
Programmatically
use Backstage\Redirects\Laravel\Models\Redirect; // Permanent redirect (301) Redirect::create([ 'source' => '/old-blog-post', 'destination' => '/new-blog-post', 'code' => 301, ]); // Temporary redirect (302) Redirect::create([ 'source' => '/maintenance', 'destination' => '/under-construction', 'code' => 302, ]); // Wildcard redirect (matches any URL containing the source) Redirect::create([ 'source' => '/blog/category/', 'destination' => '/articles/', 'code' => 301, ]);
Via Tinker
php artisan tinker
Redirect::create([ 'source' => 'example.com/old-url', 'destination' => 'example.com/new-url', 'code' => 301, ]);
Automatic Redirects via Events
The package includes an event-listener system to automatically create redirects when URLs change. This is useful when updating slugs or moving content:
use Backstage\Redirects\Laravel\Events\UrlHasChanged; // When a blog post URL changes event(new UrlHasChanged( oldUrl: 'https://example.com/old-slug', newUrl: 'https://example.com/new-slug', code: 301 ));
This automatically creates a redirect in the database:
// Created automatically by the listener Redirect::create([ 'source' => 'https://example.com/old-slug', 'destination' => 'https://example.com/new-slug', 'code' => 301, ]);
Integration Example with Eloquent Models:
use Backstage\Redirects\Laravel\Events\UrlHasChanged; use Illuminate\Database\Eloquent\Model; class BlogPost extends Model { protected static function booted() { static::updating(function ($post) { if ($post->isDirty('slug')) { $oldUrl = route('blog.show', $post->getOriginal('slug')); $newUrl = route('blog.show', $post->slug); event(new UrlHasChanged($oldUrl, $newUrl, 301)); } }); } }
Query String Handling
The package automatically preserves query strings from the source URL and appends them to the destination:
Redirect::create([ 'source' => '/old-page', 'destination' => '/new-page', 'code' => 301, ]);
When a user visits:
/old-page?utm_source=email&utm_campaign=newsletter
They are redirected to:
/new-page?utm_source=email&utm_campaign=newsletter
If the destination already has query parameters:
Redirect::create([ 'source' => '/old-page', 'destination' => '/new-page?foo=bar', 'code' => 301, ]);
Visiting /old-page?baz=qux redirects to:
/new-page?foo=bar&baz=qux
Tracking Redirect Hits
Every time a redirect is triggered, the hits counter increments automatically:
$redirect = Redirect::where('source', '/old-page')->first(); echo $redirect->hits; // Number of times this redirect was used
Use this data for analytics and monitoring:
// Most used redirects $popular = Redirect::orderBy('hits', 'desc')->take(10)->get(); // Recently created redirects $recent = Redirect::latest()->take(10)->get(); // Unused redirects (candidates for removal) $unused = Redirect::where('hits', 0)->get();
Middleware
The package includes three middleware classes, each with different matching strategies. They run in the order defined in config/redirects.php.
HttpRedirects
Matches URLs with protocol and www variations normalized:
Redirect::create([ 'source' => 'example.com/page', 'destination' => 'example.com/new-page', 'code' => 301, ]);
This matches all of these URLs:
http://example.com/pagehttps://example.com/pagehttp://www.example.com/pagehttps://www.example.com/page
WildRedirects
Performs partial URL matching using contains():
Redirect::create([ 'source' => '/blog/', 'destination' => '/articles/', 'code' => 301, ]);
This matches:
/blog/post-1→/articles//blog/category/tech→/articles//old-blog/archive→/articles/
StrictRedirects
Exact URL matching without query strings:
Redirect::create([ 'source' => 'example.com/exact-page', 'destination' => 'example.com/new-exact-page', 'code' => 301, ]);
This only matches:
http://example.com/exact-pagehttps://example.com/exact-pagehttp://www.example.com/exact-page
But NOT:
example.com/exact-page/sub-pageexample.com/other-exact-page
Customizing Middleware Order
The middleware runs in the order defined in your config. First match wins:
'middleware' => [ // 1. Try HTTP matching first (protocol/www normalized) Backstage\Redirects\Laravel\Http\Middleware\HttpRedirects::class, // 2. Then wildcard matching Backstage\Redirects\Laravel\Http\Middleware\WildRedirects::class, // 3. Finally exact matching Backstage\Redirects\Laravel\Http\Middleware\StrictRedirects::class, ],
You can reorder or remove middleware as needed. For example, to only use exact matching:
'middleware' => [ Backstage\Redirects\Laravel\Http\Middleware\StrictRedirects::class, ],
Advanced Usage
Custom Redirect Model
Create your own model extending the base Redirect model:
namespace App\Models; use Backstage\Redirects\Laravel\Models\Redirect as BaseRedirect; class Redirect extends BaseRedirect { // Add custom scopes public function scopeActive($query) { return $query->where('active', true); } // Add relationships public function user() { return $this->belongsTo(User::class); } // Override redirect logic public function redirect(Request $request): ?RedirectResponse { // Custom logic before redirect \Log::info("Redirecting from {$this->source} to {$this->destination}"); return parent::redirect($request); } }
Update your config:
'model' => App\Models\Redirect::class,
Managing Redirects Programmatically
Bulk Creation:
$redirects = [ ['source' => '/old-1', 'destination' => '/new-1', 'code' => 301], ['source' => '/old-2', 'destination' => '/new-2', 'code' => 301], ['source' => '/old-3', 'destination' => '/new-3', 'code' => 301], ]; foreach ($redirects as $redirect) { Redirect::create($redirect); }
Import from CSV:
use Illuminate\Support\Facades\Storage; use League\Csv\Reader; $csv = Reader::createFromPath(Storage::path('redirects.csv')); $csv->setHeaderOffset(0); foreach ($csv->getRecords() as $record) { Redirect::create([ 'source' => $record['source'], 'destination' => $record['destination'], 'code' => $record['code'] ?? 301, ]); }
Conditional Redirects:
// Only redirect if destination exists if (Route::has('new-route')) { Redirect::create([ 'source' => '/old-route', 'destination' => route('new-route'), 'code' => 301, ]); }
Redirect Chains (avoid these):
// BAD: Creates a redirect chain // /page-1 → /page-2 → /page-3 Redirect::create(['source' => '/page-1', 'destination' => '/page-2', 'code' => 301]); Redirect::create(['source' => '/page-2', 'destination' => '/page-3', 'code' => 301]); // GOOD: Direct redirect Redirect::create(['source' => '/page-1', 'destination' => '/page-3', 'code' => 301]);
API Reference
Redirect Model
Properties:
ulid(string) - Primary keysource(string) - Source URLdestination(string) - Destination URLcode(int) - HTTP status codehits(int) - Number of redirects performedcreated_at(timestamp)updated_at(timestamp)
Methods:
// Perform the redirect public function redirect(Request $request): ?RedirectResponse // Increment hits counter (called automatically) public function increment('hits'): void
Events
UrlHasChanged Event:
use Backstage\Redirects\Laravel\Events\UrlHasChanged; event(new UrlHasChanged( oldUrl: 'https://example.com/old', newUrl: 'https://example.com/new', code: 301 // Optional, defaults to 301 ));
Properties:
oldUrl(string) - The old URLnewUrl(string) - The new URLcode(int) - HTTP status code (default: 301)
Listeners
RedirectOldUrlToNewUrl Listener:
Automatically creates a redirect when UrlHasChanged event is dispatched.
HTTP Status Codes
Understanding when to use each status code:
| Code | Name | Use Case | Cached by Browsers |
|---|---|---|---|
| 301 | Moved Permanently | Permanent content relocation, old URL will never be used again | Yes |
| 302 | Found | Temporary redirect, old URL may be used again | No |
| 307 | Temporary Redirect | Temporary redirect that preserves HTTP method (POST stays POST) | No |
| 308 | Permanent Redirect | Permanent redirect that preserves HTTP method (POST stays POST) | Yes |
Recommendations:
- Use 301 for most permanent redirects (blog posts, pages, renamed resources)
- Use 302 for temporary situations (maintenance pages, A/B testing)
- Use 307 when redirecting form submissions temporarily
- Use 308 when permanently moving an API endpoint that receives POST/PUT/DELETE requests
Testing
Run the test suite:
composer test
Run tests with coverage:
composer test-coverage
Run static analysis:
composer analyse
Fix code style:
composer format
Writing Tests:
use Backstage\Redirects\Laravel\Models\Redirect; it('redirects old URL to new URL', function () { Redirect::create([ 'source' => '/old', 'destination' => '/new', 'code' => 301, ]); $response = $this->get('/old'); $response->assertRedirect('/new'); $response->assertStatus(301); }); it('preserves query strings', function () { Redirect::create([ 'source' => '/old', 'destination' => '/new', 'code' => 301, ]); $response = $this->get('/old?foo=bar'); $response->assertRedirect('/new?foo=bar'); }); it('increments hits counter', function () { $redirect = Redirect::create([ 'source' => '/old', 'destination' => '/new', 'code' => 301, ]); expect($redirect->hits)->toBe(0); $this->get('/old'); expect($redirect->fresh()->hits)->toBe(1); });
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details on how to contribute to this package.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Security Considerations:
- Validate redirect destinations to prevent open redirects
- Sanitize user input when creating redirects programmatically
- Monitor for redirect loops and chains
- Implement rate limiting to prevent abuse
- Use HTTPS for all redirect destinations when possible
Credits
- Mark van Eijk - Creator and maintainer
- All Contributors
License
The MIT License (MIT). Please see License File for more information.
Support
Built with by Backstage CMS