spatie/laravel-missing-page-redirector
Automatically redirect 404 (missing) pages in Laravel to preserve SEO during site migrations. Configure old-to-new URL redirects in a config file or implement a custom redirector (e.g., database-backed) via middleware in your global stack.
Installation:
composer require spatie/laravel-missing-page-redirector
Publish the config file:
php artisan vendor:publish --provider="Spatie\MissingPageRedirector\MissingPageRedirectorServiceProvider"
First Use Case:
Edit config/missing-page-redirector.php and add your first redirect rule:
'redirects' => [
'old-url' => '/new-url',
'another-old-path' => 'https://example.com/new-location',
],
Test by visiting your-app.test/old-url—it should now redirect to /new-url.
Key Files:
config/missing-page-redirector.php: Central configuration for redirects.app/Providers/MissingPageRedirectorServiceProvider.php: Service provider (auto-registered).Configuration-Driven Redirects:
Use the redirects array in the config file for static redirects. Ideal for small-scale migrations or one-off changes:
'redirects' => [
'blog/old-post' => '/blog/new-post',
'legacy/contact' => '/contact-us',
],
Database-Backed Redirects:
For dynamic or large-scale redirects, create a custom redirector by implementing Spatie\MissingPageRedirector\Contracts\Redirector:
// Example: Database-backed redirector
use Spatie\MissingPageRedirector\Contracts\Redirector;
class DatabaseRedirector implements Redirector
{
public function getRedirects(): array
{
return DB::table('redirects')->pluck('new_path', 'old_path')->toArray();
}
}
Register it in config/missing-page-redirector.php:
'redirector' => \App\Redirectors\DatabaseRedirector::class,
Middleware Integration:
The package auto-registers a middleware (HandleMissingPageRedirects) in App\Http\Kernel. Ensure it’s placed before HandleMissingPageRedirects in the $middleware stack if you’re customizing the HTTP kernel.
Conditional Redirects: Use closures in the config for logic-based redirects (e.g., user roles, time-based):
'redirects' => [
'admin/old-dashboard' => function ($request) {
return auth()->check() ? '/admin/new-dashboard' : abort(404);
},
],
Wildcard Redirects: Leverage regex or pattern matching for bulk redirects:
'redirects' => [
'blog/202*' => '/blog/archives/202$0', // Redirects /blog/2021/* to /blog/archives/2021/*
],
Fallback Redirects: Define a default redirect for unmatched paths:
'fallback_redirect' => '/',
spatie/laravel-seo-tools to update meta tags dynamically during redirects.HandleMissingPageRedirects middleware for post-migration analysis:
public function handle($request, Closure $next)
{
if ($redirect = resolve(MissingPageRedirector::class)->getRedirect($request->path())) {
info("Redirect triggered: {$request->path()} -> {$redirect}");
}
return $next($request);
}
MissingPageRedirector::getRedirect() in PHPUnit tests:
$this->assertEquals(
'/new-url',
resolve(MissingPageRedirector::class)->getRedirect('old-url')
);
Middleware Priority:
HandleMissingPageRedirects runs after route resolution (e.g., after VerifyCsrfToken).$middleware stack for web group:
protected $middleware = [
\Spatie\MissingPageRedirector\Middleware\HandleMissingPageRedirects::class,
// ... other middleware
];
Case Sensitivity:
/Old-Url won’t match /old-url.'redirects' => [
strtolower('Old-Url') => '/new-url',
],
Circular Redirects:
A → B and B → A creates infinite loops.MissingPageRedirector::getRedirect() method to validate paths before adding redirects:
$redirector = resolve(MissingPageRedirector::class);
if (!$redirector->getRedirect('new-path')) {
// Safe to add redirect
}
Query Strings:
/old?param=1 won’t match /old).'redirects' => [
'old' => '/new', // Matches /old, /old?param=1, etc.
],
Caching:
Redirector:
use Illuminate\Support\Facades\Cache;
public function getRedirects(): array
{
return Cache::remember('missing-page-redirects', now()->addHours(1), function () {
return DB::table('redirects')->pluck('new_path', 'old_path')->toArray();
});
}
Check Redirects: Use Tinker to inspect available redirects:
php artisan tinker
>>> $redirector = resolve(\Spatie\MissingPageRedirector\MissingPageRedirector::class);
>>> $redirector->getRedirect('test-path');
Log Missing Redirects: Add logging to identify unmatched paths:
// In HandleMissingPageRedirects middleware
if (!$redirect = $redirector->getRedirect($request->path())) {
info("No redirect found for: {$request->path()}");
}
Custom Redirect Logic:
Extend the MissingPageRedirector class to add pre/post-redirect hooks:
class CustomMissingPageRedirector extends \Spatie\MissingPageRedirector\MissingPageRedirector
{
public function getRedirect(string $path): ?string
{
$redirect = parent::getRedirect($path);
if ($redirect && str_contains($redirect, 'analytics')) {
// Add UTM parameters
$redirect .= '?utm_source=legacy';
}
return $redirect;
}
}
Bind it in AppServiceProvider:
public function register()
{
$this->app->bind(
\Spatie\MissingPageRedirector\Contracts\MissingPageRedirector::class,
\App\Redirectors\CustomMissingPageRedirector::class
);
}
Event-Based Redirects: Dispatch events before/after redirects using Laravel’s events:
// In HandleMissingPageRedirects middleware
event(new \App\Events\MissingPageRedirectAttempt($request->path(), $redirect));
API Redirects: Expose redirects via an API endpoint for dynamic management:
Route::get('/api/redirects', function () {
return response()->json(
resolve(\Spatie\MissingPageRedirector\Contracts\Redirector::class)->getRedirects()
);
});
'redirects' => env('APP_ENV') === 'production'
? require __DIR__.'/redirects_production.php'
: [],
Redirector:
class RemoteRedirector implements Redirector
{
public function getRedirects(): array
{
return json_decode(file_get_contents('https://example.com/redirects.json'), true);
}
}
How can I help you explore Laravel packages today?