Safe Laravel redirect manager with database redirects, wildcard/regex matching, CSV import/export and hit tracking.
- Exact redirects
- Wildcard redirects
- Optional regex redirects
301,302,307,308,410status codes- Query string preservation
- Redirect loop protection
- Hit counter and
last_hit_at - CSV import/export commands
- Laravel 11, 12 and 13 support
composer require step2dev/lazy-seo-redirects
php artisan vendor:publish --tag="lazy-seo-redirects-config"
php artisan vendor:publish --tag="lazy-seo-redirects-migrations"
php artisan migrateRegister the middleware where you want redirects to be resolved.
use Step2dev\LazySeoRedirect\Http\Middleware\HandleSeoRedirects;
->withMiddleware(function ($middleware) {
$middleware->append(HandleSeoRedirects::class);
})Or use it on selected routes:
use Illuminate\Support\Facades\Route;
use Step2dev\LazySeoRedirect\Http\Middleware\HandleSeoRedirects;
Route::middleware(HandleSeoRedirects::class)->group(function () {
// routes
});use Step2dev\LazySeoRedirect\Models\SeoRedirect;
SeoRedirect::create([
'old_url' => '/old-page',
'new_url' => '/new-page',
'status_code' => 301,
'enabled' => true,
]);Gone response:
SeoRedirect::create([
'old_url' => '/removed-page',
'new_url' => null,
'status_code' => 410,
'enabled' => true,
]);Wildcard:
SeoRedirect::create([
'old_url' => '/blog/*',
'new_url' => '/articles',
'status_code' => 308,
]);Regex redirects are disabled by default. Enable them only when needed:
'regex_enabled' => true,SeoRedirect::create([
'old_url' => '#^old/(.*)$#',
'new_url' => '/new/$1',
'status_code' => 307,
'is_regex' => true,
]);Use the model directly when you need redirects in your own admin panel, API, dashboard, sitemap tools or reports.
use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$redirects = SeoRedirect::query()
->enabled()
->latest()
->get();use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$redirects = SeoRedirect::query()
->latest()
->paginate(20);use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$search = request('search');
$redirects = SeoRedirect::query()
->when($search, function ($query, string $search): void {
$query->where(function ($query) use ($search): void {
$query->where('old_url', 'like', "%{$search}%")
->orWhere('new_url', 'like', "%{$search}%");
});
})
->latest()
->paginate(20);use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$redirects = SeoRedirect::query()
->enabled()
->orderByDesc('hits')
->limit(10)
->get();use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$redirects = SeoRedirect::query()
->where('enabled', false)
->latest()
->get();use Step2dev\LazySeoRedirect\Models\SeoRedirect;
$redirect = SeoRedirect::query()
->where('normalized_old_url_hash', sha1(SeoRedirect::normalizePath('/old-page')))
->first();Publish the config and change the table name before running migrations:
// config/lazy-seo-redirects.php
return [
'table' => 'seo_redirects',
];php artisan lazy-seo-redirects:import redirects.csv
php artisan lazy-seo-redirects:import redirects.csv --no-update
php artisan lazy-seo-redirects:export redirects.csvCSV columns:
old_url,new_url,status_code,enabled,is_regex
/old,/new,301,1,0return [
'enabled' => true,
'table' => 'seo_redirects',
'cache_seconds' => 60,
'preserve_query' => true,
'wildcard_enabled' => true,
'regex_enabled' => false,
'allowed_status_codes' => [301, 302, 307, 308, 410],
'security' => [
'allow_external_destinations' => false,
'allowed_hosts' => [],
'block_protocol_relative_urls' => true,
],
];composer test
composer analyse
composer format