rayzenai/url-manager
Laravel package to manage URLs, redirects, SEO metadata, visit tracking, and XML sitemaps, with redirect-loop protection and automatic old→new slug redirects. Includes Filament 4 admin panel integration (UrlInput) and optional media SEO via file-manager.
A comprehensive Laravel package for managing URLs, redirects, and sitemaps with Filament admin panel integration.
composer require rayzenai/url-manager
For complete media SEO functionality, install the companion file-manager package:
composer require kirantimsina/file-manager
This package provides:
php artisan vendor:publish --tag=url-manager-config
The URL Manager uses the Stevebauman/Location package to detect visitor countries from IP addresses. You need to set up MaxMind's GeoIP database:
GeoLite2-City.mmdbdatabase/maxmind/GeoLite2-City.mmdbphp artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider"
config/location.php:'driver' => Stevebauman\Location\Drivers\MaxMind::class,
'maxmind' => [
'local' => [
'type' => 'city', // or 'country' for smaller file
'path' => database_path('maxmind/GeoLite2-City.mmdb'),
],
],
Configure MaxMind web service in your .env:
MAXMIND_USER_ID=your_user_id
MAXMIND_LICENSE_KEY=your_license_key
php artisan vendor:publish --tag=url-manager-migrations
php artisan migrate
This will create the following tables:
urls - For managing URLs and redirectsurl_visits - For tracking visitor analyticsgoogle_search_console_settings - For storing Google Search Console credentials securelyAdd the plugin to your Filament panel configuration (typically in app/Providers/Filament/AdminPanelProvider.php):
use RayzenAI\UrlManager\UrlManagerPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ... other configuration
->plugin(UrlManagerPlugin::make());
}
Add the HasUrl trait to any model that needs URL management:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use RayzenAI\UrlManager\Traits\HasUrl;
class Product extends Model
{
use HasUrl;
protected $fillable = [
'name',
'slug',
'description',
'is_active', // Or 'active' - configurable via activeUrlField() method
// ... other fields
];
/**
* Define the URL path for this model
* Required by HasUrl trait
*/
public function webUrlPath(): string
{
return 'products/' . $this->slug;
}
/**
* Define the active field name (optional)
* Override this if your model uses 'active' instead of 'is_active'
*/
public function activeUrlField(): string
{
return 'active'; // Default is 'is_active'
}
/**
* Enable automatic view count tracking
* Optional - implement this to track views on your model
*/
public function getViewCountColumn(): ?string
{
return 'view_count'; // Return null if no view counting needed
}
/**
* Define Open Graph tags for SEO
* Optional but recommended
*/
public function ogTags(): array
{
return [
'title' => $this->name,
'description' => $this->description,
'image' => $this->image_url,
'type' => 'product',
];
}
/**
* Define sitemap change frequency
* Optional - defaults to 'weekly'
*/
public function getSitemapChangefreq(): string
{
return 'daily';
}
}
To enable automatic redirect handling when slugs are updated, add the middleware to your bootstrap/app.php:
use RayzenAI\UrlManager\Http\Middleware\HandleUrlRedirects;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware): void {
// Add redirect handler EARLY, before route model binding
// This is critical so old URLs redirect before hitting 404
$middleware->web(prepend: [
HandleUrlRedirects::class,
]);
// ... rest of your middleware configuration
})
// ... rest of configuration
Why prepend? The middleware must run BEFORE route model binding. If you have a route like /leader/{leader:slug} and someone visits /leader/old-slug, the middleware catches it and redirects to /leader/new-slug before Laravel tries (and fails) to find a model with slug='old-slug'.
What it does:
?page=2)Before you start using URL Manager, check that all your models are properly configured:
# Check all models using HasUrl trait
php artisan url-manager:check
# Check a specific model
php artisan url-manager:check "App\Models\Product"
This command will verify:
webUrlPath() method is implementedis_active (or custom active field) exists in databasegetViewCountColumn() is configured correctlyogTags(), getSeoMetadata()) are presentGenerate a new model with HasUrl trait and all required methods pre-configured:
# Create a basic model
php artisan url-manager:make-model Product
# Create model with migration
php artisan url-manager:make-model Product --migration
# Create model with migration, factory, and seeder
php artisan url-manager:make-model Product --all
The generated model includes:
webUrlPath() method with sensible defaultsgetViewCountColumn() for automatic view trackingogTags() and getSeoMetadata() for SEOFor common URL operations, use the UrlManager facade:
use RayzenAI\UrlManager\Facades\UrlManager;
// Generate URL for a model
$product = Product::find(1);
UrlManager::generateUrl($product);
// Track a visit manually
UrlManager::trackVisit($product, auth()->id());
// Create a redirect
UrlManager::createRedirect('old-url', 'new-url', 301);
// Find URL by slug
$url = UrlManager::findBySlug('products/my-product');
// Get visit count
$visits = UrlManager::getVisitCount($product);
// Delete URL
UrlManager::deleteUrl($product);
Generate URLs for all models that use the HasUrl trait:
php artisan urls:generate
Or for a specific model:
php artisan urls:generate "App\Models\Product"
For large datasets with thousands of records, you may need to increase PHP memory and execution limits:
# Increase PHP memory limit and execution time
php -d memory_limit=2G -d max_execution_time=0 artisan urls:generate
# Or generate for specific models one at a time
php artisan urls:generate "App\Models\Product"
php artisan urls:generate "App\Models\Category"
php artisan urls:generate "App\Models\Blog"
use RayzenAI\UrlManager\Models\Url;
// Create a permanent redirect
Url::createRedirect('old-page', 'new-page', 301);
// Create a temporary redirect
Url::createRedirect('summer-sale', 'products/sale', 302);
Generate a sitemap with all active URLs:
php artisan sitemap:generate
For large sites (>10,000 URLs), the package automatically creates multiple sitemap files with an index.
You can include static routes in your sitemap that aren't tied to database models. This is useful for:
Configure custom routes in config/url-manager.php:
'sitemap' => [
'custom_routes' => [
[
'path' => '/about',
'priority' => 0.7,
'changefreq' => 'monthly',
'lastmod' => null, // Optional: Carbon instance or date string
],
[
'path' => '/contact',
'priority' => 0.6,
'changefreq' => 'yearly',
],
[
'path' => '/blog',
'priority' => 0.8,
'changefreq' => 'daily',
'lastmod' => now(), // Can use Carbon instance
],
],
],
Available Options:
path (required): The route path (e.g., /about, /contact)priority (optional): SEO priority from 0.0 to 1.0 (default: 0.5)changefreq (optional): How often the page changes: always, hourly, daily, weekly, monthly, yearly, never (default: weekly)lastmod (optional): Last modification date as Carbon instance or date string (default: current time)These routes will be automatically included when you run:
php artisan sitemap:generate
Since Google deprecated the ping endpoint in June 2023 and Bing has also discontinued their ping service, API credentials are now required for automated sitemap submission.
Prerequisites:
Create a Google Cloud Project:
Create a Service Account:
Add Service Account to Search Console:
service-account@project.iam.gserviceaccount.com)Configure in Admin Panel:
https://www.yoursite.com (must match exactly)sc-domain:yoursite.com (recommended - covers all subdomains and protocols)Why Database Storage?
Once configured, you can submit sitemaps in multiple ways:
Via Admin Panel:
Via Command Line:
php artisan sitemap:submit
Programmatically:
use RayzenAI\UrlManager\Services\GoogleSearchConsoleService;
// Submit to Google only
$result = GoogleSearchConsoleService::submitGoogleSitemap();
// Submit to all search engines (Google + Bing note)
$result = GoogleSearchConsoleService::submitToAllSearchEngines();
Service Account Issues:
sc-domain:yoursite.com formatGeneral Issues:
Bing has also discontinued their ping endpoint. Sitemaps must be manually submitted through Bing Webmaster Tools.
// Get the full URL for a model
$product = Product::find(1);
echo $product->webUrl(); // https://yoursite.com/products/my-product
// Get the admin URL
echo $product->adminUrl(); // /admin/products/1/edit
// Check if a model's URL is active
if ($product->url && $product->url->status === 'active') {
// URL is active
}
The package automatically tracks URL visits, but to track view counts on your models, you need to implement the getViewCountColumn() method:
class Product extends Model
{
use HasUrl;
protected $fillable = [
'name',
'slug',
'view_count', // Add your view count column
// ...
];
/**
* Enable automatic view count tracking
* When visitors access this model's URL, the view_count column will be incremented
*/
public function getViewCountColumn(): ?string
{
return 'view_count'; // Return null if you don't want view counting
}
}
Important: Make sure your database migration includes the view count column:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->unsignedBigInteger('view_count')->default(0); // Add this
$table->boolean('is_active')->default(true);
$table->timestamps();
});
The package provides two ways to track visits:
Register the middleware in your application:
For Laravel 11 - Add to bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'track-url-visits' => \RayzenAI\UrlManager\Http\Middleware\TrackUrlVisits::class,
]);
})
For Laravel 10 and below - Add to app/Http/Kernel.php:
protected $middlewareAliases = [
// ...
'track-url-visits' => \RayzenAI\UrlManager\Http\Middleware\TrackUrlVisits::class,
];
Then apply the middleware to your routes:
// For Livewire components
Route::get('/property/{slug}', PropertyDetails::class)
->middleware('track-url-visits')
->name('property');
// For API routes
Route::middleware(['track-url-visits'])->group(function () {
Route::get('/api/products/{slug}', [ProductController::class, 'show']);
Route::get('/api/categories/{slug}', [CategoryController::class, 'show']);
});
// Works with any route type - controllers, closures, Livewire, Inertia, etc.
Route::middleware(['auth', 'track-url-visits'])->group(function () {
Route::get('/dashboard', Dashboard::class);
Route::get('/profile', [ProfileController::class, 'show']);
});
The middleware automatically:
getViewCountColumn() is implementedIf you use the package's fallback route controller:
// Add at the END of your routes/web.php
Route::fallback([\RayzenAI\UrlManager\Http\Controllers\UrlController::class, 'handle']);
Visits are automatically tracked for any URL managed by the package.
// URL-level visit tracking (always available)
$url = $product->url;
echo $url->visits; // Total visits on the URL record
echo $url->last_visited_at; // Last visit timestamp
// Model-level view tracking (if getViewCountColumn() is implemented)
echo $product->view_count; // Total views on the model itself
Note: The difference between URL visits and model view counts:
urls table and url_visits table (always enabled)view_count column (requires getViewCountColumn() implementation)The URL Manager provides comprehensive visitor tracking with the following features:
The package intelligently detects mobile app traffic through:
source=android or source=ios parametersIf you have existing visitor data without country codes, run:
php artisan url-manager:populate-country-codes
This command will:
The package includes a built-in test page to verify that referrer tracking is working correctly. This is especially useful when setting up the package or debugging referrer capture issues.
Access the test page (only available in non-production environments):
https://yourdomain.com/_url-manager/test
The test page provides:
How it works:
/_url-manager/testentities/my-product, blog/my-post)The test page uses a multi-layer approach to ensure referrers are captured:
?ref= query parameter)Note: The test route is automatically disabled in production environments for security.
The configuration file config/url-manager.php allows you to customize:
return [
// Database table name
'table_name' => 'urls',
// URL types available in your application
'types' => [
'product' => 'Product',
'category' => 'Category',
'page' => 'Page',
// Add your custom types
],
// Maximum redirect chain depth (prevents infinite loops)
'max_redirect_depth' => 5,
// Visit tracking
'track_visits' => true,
'visit_queue' => 'low', // Queue for visit tracking jobs
// Sitemap configuration
'sitemap' => [
'enabled' => true,
'path' => public_path('sitemap.xml'),
'max_urls_per_file' => 10000,
'default_changefreq' => 'weekly',
'default_priority' => 0.5,
'priorities' => [
'product' => 0.8,
'category' => 0.9,
'page' => 0.6,
],
// Custom static routes to include in sitemap
'custom_routes' => [
[
'path' => '/about',
'priority' => 0.7,
'changefreq' => 'monthly',
'lastmod' => null, // Optional: Carbon instance or date string
],
[
'path' => '/contact',
'priority' => 0.6,
'changefreq' => 'yearly',
],
],
// Image sitemap configuration
'images' => [
'enabled' => true,
'max_images_per_file' => 5000,
'image_size' => 'large', // Use optimized size: 'thumb', 'medium', 'large', 'full', or null for original
],
],
// Filament admin panel
'filament' => [
'enabled' => true,
'navigation_group' => 'System',
'navigation_icon' => 'heroicon-o-link',
'navigation_sort' => 100,
],
];
The UrlManager facade provides a convenient API for common URL operations:
use RayzenAI\UrlManager\Facades\UrlManager;
// Generate or update URL for a model
$url = UrlManager::generateUrl($product);
// Manually track a visit (normally handled automatically)
UrlManager::trackVisit($product, auth()->id(), ['source' => 'mobile']);
// Create redirects programmatically
UrlManager::createRedirect('/old-path', '/new-path', 301);
// Find URLs by slug
$url = UrlManager::findBySlug('products/my-product');
// Get all redirects
$redirects = UrlManager::getRedirects();
// Get visit count for a model
$totalVisits = UrlManager::getVisitCount($product);
// Delete URL for a model
UrlManager::deleteUrl($product);
When to use the facade:
When NOT to use it:
Register custom URL types in your configuration:
'types' => [
'product' => 'Product',
'article' => 'Article',
'custom_type' => 'Custom Type',
],
Models can provide SEO metadata through the getSeoMetadata() method:
public function getSeoMetadata(): array
{
return [
'title' => $this->seo_title ?? $this->name,
'description' => $this->seo_description ?? $this->description,
'keywords' => $this->seo_keywords,
'og_image' => $this->featured_image,
'og_type' => 'article',
'twitter_card' => 'summary_large_image',
];
}
Listen for URL events in your application:
// In a service provider or event listener
Event::listen('url-manager.url.visited', function ($url, $model) {
// Log visit, send analytics, etc.
Log::info("URL visited: {$url->slug}");
});
If you have the kirantimsina/file-manager package installed, you can enhance your SEO by managing media metadata:
Generate SEO-friendly titles for all your media files:
# Generate SEO titles for all media
php artisan file-manager:populate-seo-titles
# Generate for specific model only
php artisan file-manager:populate-seo-titles --model=Product
# Dry run to see what would be generated
php artisan file-manager:populate-seo-titles --dry-run
# Overwrite existing SEO titles
php artisan file-manager:populate-seo-titles --overwrite
The command automatically generates SEO-friendly titles based on:
Generate a dedicated image sitemap for better image SEO:
# Generate image sitemap with optimized SEO titles
php artisan sitemap:generate-images
# Include specific models only
php artisan sitemap:generate-images --model=Product --model=Blog
# Set custom maximum images per sitemap file
php artisan sitemap:generate-images --max-urls=5000
Features:
Performance Optimization:
Configure the image size used in sitemaps in config/url-manager.php:
'sitemap' => [
'images' => [
'enabled' => true,
'max_images_per_file' => 5000,
// Configure which size to use for sitemap images
// Options: 'icon', 'thumb', 'medium', 'large', 'full', etc.
// Set to null to use original images
'image_size' => 'large', // Default: 720px height
],
],
The available sizes are defined in your config/file-manager.php:
'image_sizes' => [
'icon' => 64, // 64px height
'thumb' => 240, // 240px height
'medium' => 480, // 480px height
'large' => 720, // 720px height (recommended for sitemaps)
'full' => 1080, // 1080px height
],
Control which models receive SEO titles in config/file-manager.php:
'seo' => [
'enabled_models' => [
'App\Models\Product',
'App\Models\Category',
'App\Models\Blog',
// Models that should have SEO titles
],
'excluded_models' => [
'App\Models\User',
'App\Models\Order',
// Models that should NOT have SEO titles
],
],
When using file-manager, media files are automatically included in your sitemaps with proper SEO titles and metadata for better search engine indexing. The integration:
For sites with more than 10,000 URLs, the package automatically generates multiple sitemap files:
sitemap.xml (index file)
sitemap-0.xml (first 10,000 URLs)
sitemap-1.xml (next 10,000 URLs)
...
The package includes a complete Filament resource with:
The package provides two dashboard widgets:
The package provides a UrlInput form component for managing slugs in Filament forms:
use RayzenAI\UrlManager\Filament\Forms\Components\UrlInput;
UrlInput::make('slug')
->sourceField('name') // Auto-generate from name field
->forModel(Product::class) // For proper validation
Allowing Slug Updates:
By default, slugs are disabled when editing records to prevent breaking existing URLs. To allow updates (with automatic redirect creation):
UrlInput::make('slug')
->allowUpdatingSlug() // Enables editing on existing records
How it works:
HasUrl traitExample:
kp-oli with URL /leader/kp-olikp-sharma-oli in Filament/leader/kp-sharma-oli (points to leader)/leader/kp-oli → /leader/kp-sharma-oliis_active or active) in models using HasUrl traitslugField() if using different name)webUrlPath() method to define URL structureactiveUrlField() method if using a field name other than is_activeUrlInput::make('slug')->allowUpdatingSlug() for automatic redirect creationRun the package tests:
composer test
Ensure your model:
HasUrl traitis_active or active - configurable via activeUrlField() method)webUrlPath() method"URL with slug already exists for different model" Warning
Not all URLs are generated
Models using 'active' instead of 'is_active'
activeUrlField() method in your model:public function activeUrlField(): string
{
return 'active'; // Your model's active field name
}
Check that:
Verify:
HandleUrlRedirects middleware is registered with prepend (not append) in bootstrap/app.phpurl-manager.max_redirect_depth config)Contributions are welcome! Please submit pull requests with tests.
MIT License. See LICENSE file for details.
For issues and questions, please use the GitHub issue tracker.
Created by Kiran Timsina at RayzenAI.
How can I help you explore Laravel packages today?