rayzenai / url-manager
Laravel URL management package with Filament integration for SEO-friendly URLs, redirects, and sitemap generation
Requires
- php: ^8.3
- filament/filament: ^4.0
- google/apiclient: ^2.0
- illuminate/support: ^11.0|^12.0
- jenssegers/agent: ^2.6
- laravel/sanctum: ^4.0
- spatie/laravel-package-tools: ^1.16
- spatie/laravel-sitemap: ^7.0
- stevebauman/location: ^7.0
README
A comprehensive Laravel package for managing URLs, redirects, and sitemaps with Filament admin panel integration.
Features
- 🔗 Dynamic URL Management - Manage all your application URLs from a central location
- 🔄 301/302 Redirects - Create and manage URL redirects with configurable status codes
- 🗺️ Automatic Sitemap Generation - Generate XML sitemaps with support for large sites
- 📊 Visit Tracking - Track URL visits with country detection, device info, and mobile app support
- 🎨 Filament Integration - Full-featured admin panel for URL management
- 🏷️ SEO Metadata - Manage meta tags and Open Graph data
- 🚀 Performance Optimized - Efficient database queries with proper indexing
- 🔒 Redirect Loop Protection - Automatic detection and prevention of circular redirects
Requirements
- PHP 8.2+
- Laravel 11.0+ or 12.0+
- Filament 4.0+
- Stevebauman/Location 7.0+ with MaxMind database (for visitor country detection)
- kirantimsina/file-manager (optional, for media SEO functionality)
Installation
Step 1: Install via Composer
composer require rayzenai/url-manager
Optional: Install File Manager for Enhanced SEO
For complete media SEO functionality, install the companion file-manager package:
composer require kirantimsina/file-manager
This package provides:
- Media metadata tracking with SEO titles
- Image optimization and compression
- Enhanced file upload components for Filament
Step 2: Publish Configuration
php artisan vendor:publish --tag=url-manager-config
Step 3: Configure Location Service (Required for visitor tracking)
The URL Manager uses the Stevebauman/Location package to detect visitor countries from IP addresses. You need to set up MaxMind's GeoIP database:
Option A: Use Local Database (Recommended)
- Download the free GeoLite2 City database from MaxMind
- Create a free account and download
GeoLite2-City.mmdb
- Place the file in your Laravel project:
database/maxmind/GeoLite2-City.mmdb
- Publish and configure the Location package:
php artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider"
- Update
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'), ], ],
Option B: Use Web Service
Configure MaxMind web service in your .env
:
MAXMIND_USER_ID=your_user_id MAXMIND_LICENSE_KEY=your_license_key
Step 4: Run Migrations
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 securely
Step 5: Register with Filament
Add 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()); }
Step 5: Configure Your Models
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' } /** * 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'; } }
Step 6: Register Routes (Optional)
If you want to use the package's URL handling, add this to your routes/web.php
:
// Add at the END of your routes file (must be last) Route::fallback([\RayzenAI\UrlManager\Http\Controllers\UrlController::class, 'handle']);
Usage
Creating URLs for Existing Models
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"
Handling Large Datasets
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"
Creating Redirects
Via Filament Admin
- Navigate to the URLs section in your Filament admin panel
- Click "Create Redirect"
- Enter the source and destination URLs
- Choose redirect type (301 or 302)
Programmatically
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);
Generating Sitemaps
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.
Submitting Sitemaps to Search Engines
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.
Setting up Google Search Console API
Prerequisites:
- A verified property in Google Search Console
- A Google Cloud Project with billing enabled (API has free tier)
Using Service Account Authentication
-
Create a Google Cloud Project:
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable these APIs:
- "Google Search Console API"
- "Search Console API" (if available)
- Note: The "Indexing API" is separate and not needed for sitemap submission
-
Create a Service Account:
- Navigate to "IAM & Admin" > "Service Accounts"
- Click "Create Service Account"
- Give it a name like "sitemap-submitter"
- Click "Create and Continue"
- Skip the optional role assignment (click "Continue")
- Click "Done"
- Find your new service account in the list and click on it
- Go to the "Keys" tab
- Click "Add Key" > "Create New Key"
- Select "JSON" format
- Click "Create" to download the JSON credentials file
- Keep this file secure - it contains credentials for API access
-
Add Service Account to Search Console:
- Go to Google Search Console
- Select your property
- Go to Settings > Users and permissions
- Click "Add user"
- Enter the service account email (found in the JSON file, looks like
service-account@project.iam.gserviceaccount.com
) - Select "Owner" permission level
- Click "Add"
-
Configure in Admin Panel:
- Navigate to the Google Search Console settings page in your Filament admin panel
- Toggle "Enable Google Search Console Integration" to ON
- Enter your site URL or domain property:
- For URL-prefix property:
https://www.yoursite.com
(must match exactly) - For Domain property:
sc-domain:yoursite.com
(recommended - covers all subdomains and protocols)
- For URL-prefix property:
- Add your Service Account credentials:
- Open your downloaded JSON file in a text editor
- Copy the entire JSON content
- Paste it into the "Service Account JSON" field
- The service account email will be automatically extracted
- Click "Save Settings"
- Use "Test Connection" to verify the setup is working
Why Database Storage?
- Credentials are encrypted and stored securely in your database
- Survives deployments (no need to re-upload files)
- No file system dependencies
- Easier to manage in production environments
Submitting Sitemaps
Once configured, you can submit sitemaps in multiple ways:
-
Via Admin Panel:
- Go to URLs management page in Filament
- Click "Submit to Search Engines" button
-
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();
Troubleshooting Google Search Console
Service Account Issues:
- "API not configured" error: Ensure you've enabled the Google Search Console API in your Google Cloud Project
- "Site not verified" error: Make sure the service account email is added as a user in Search Console with "Owner" permissions
- "Invalid credentials" error: Check that the JSON file path is correct and the file is readable by the web server
- 403 Forbidden errors:
- The service account may not have proper permissions. Verify it's added to Search Console with "Owner" role
- Check if you're using the correct property format. If you have a domain property in Search Console, use
sc-domain:yoursite.com
format - Verify the exact property format in Search Console matches what you've configured
- Invalid JSON error: Ensure you're copying the complete JSON content from the credentials file, including all brackets
General Issues:
- "Invalid site URL" error: The site URL must match exactly with how it's registered in Search Console (including www/non-www, https/http)
- No sitemaps found: The site may not have any sitemaps submitted yet. Use "Submit Sitemap Now" button to submit
- Rate limiting: Google Search Console API has quotas. Check your Google Cloud Console for usage limits
- Connection test passes but submission fails: Check that sitemap.xml exists and is accessible at the expected URL
Note on Bing
Bing has also discontinued their ping endpoint. Sitemaps must be manually submitted through Bing Webmaster Tools.
Accessing URLs in Your Application
// 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 }
Visit Tracking
The package provides two ways to track visits:
Method 1: Using Middleware (Recommended for Livewire & API Routes)
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:
- Matches the request path against URLs in the database
- Records visits asynchronously via queued jobs
- Captures IP address, user agent, referrer, and authenticated user ID
- Works transparently without modifying your controllers or components
Method 2: Using Fallback Route
If 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.
Accessing Visit Data
$url = $product->url; echo $url->visits; // Total visits echo $url->last_visited_at; // Last visit timestamp
Visitor Analytics Features
The URL Manager provides comprehensive visitor tracking with the following features:
Country Detection
- Automatically detects visitor's country from IP address using MaxMind GeoIP database
- Displays country flags (🇺🇸 🇬🇧 🇳🇵 🇮🇳) in the admin panel
- Filter visits by country in Filament resource
Mobile App Detection
The package intelligently detects mobile app traffic through:
- API Source Parameters: Recognizes
source=android
orsource=ios
parameters - User-Agent Analysis: Detects Flutter, React Native, Expo, and other mobile frameworks
- HTTP Client Detection: Identifies OkHttp (Android) and Alamofire (iOS) clients
Populate Existing Data
If you have existing visitor data without country codes, run:
php artisan url-manager:populate-country-codes
This command will:
- Process all URL visits without country codes
- Resolve countries from IP addresses
- Update records with detected country codes
Visitor Information Tracked
- IP Address with country flag
- Country Code with flag emoji display
- Browser/App type and version
- Device Type (Desktop, Mobile, Tablet)
- Referrer URL
- User (if authenticated)
- Visit Timestamp
- Metadata (additional custom data)
Configuration
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, ], // 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, ], ];
Advanced Features
Custom URL Types
Register custom URL types in your configuration:
'types' => [ 'product' => 'Product', 'article' => 'Article', 'custom_type' => 'Custom Type', ],
SEO Metadata
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', ]; }
Event Handling
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}"); });
Media SEO with File Manager
If you have the kirantimsina/file-manager
package installed, you can enhance your SEO by managing media metadata:
Populate SEO Titles for Images
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:
- Parent model's name/title
- Media field context (e.g., "Featured Image", "Gallery")
- Clean filename processing
- Removes special characters from beginning/end for cleaner SEO
Image Sitemap Generation
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:
- Uses pre-populated SEO titles from media_metadata table for optimal performance
- Only includes images with meaningful SEO titles (excludes internal/system images)
- Automatically creates index files for large image collections
- Generates Google Image sitemap format with proper XML namespace
- Includes image location, title, and caption metadata
- Uses optimized image sizes instead of originals for better performance
Performance Optimization:
- Direct database queries avoid expensive polymorphic lookups
- Chunked processing for handling millions of images
- Only processes images from SEO-enabled models (configured in file-manager)
Image Size Configuration
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 ],
SEO Title Configuration
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 ], ],
Media Metadata in Sitemaps
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:
- Respects model configuration (enabled/excluded models)
- Uses cached SEO titles for fast generation
- Supports large-scale image collections with automatic file splitting
Multiple Sitemap Support
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)
...
Filament Admin Panel
The package includes a complete Filament resource with:
- URL Listing - Search, filter, and sort URLs
- Create/Edit Forms - Manage URL details and metadata
- Redirect Creation - Quick action to create 301/302 redirects
- Sitemap Generation - Generate and view sitemaps
- Bulk Actions - Activate/deactivate multiple URLs
- Visit Statistics - View visit counts and last visited times
Dashboard Widgets
The package provides two dashboard widgets:
- URL Stats Overview - Displays total URLs, redirects, and visit statistics
- Top URLs Table - Shows the 10 most visited URLs with their metrics
Best Practices
- Always include an active field (
is_active
oractive
) in models using HasUrl trait - Implement
webUrlPath()
method to define URL structure - Override
activeUrlField()
method if using a field name other thanis_active
- Use meaningful slugs for SEO optimization
- Set up redirects when changing URL structures
- Generate sitemaps regularly (via cron job)
- Monitor redirect chains to avoid deep nesting
- Use appropriate HTTP status codes (301 for permanent, 302 for temporary)
Testing
Run the package tests:
composer test
Troubleshooting
URLs not generating for models
Ensure your model:
- Uses the
HasUrl
trait - Has an active field (
is_active
oractive
- configurable viaactiveUrlField()
method) - Implements the
webUrlPath()
method
Common URL Generation Issues
-
"URL with slug already exists for different model" Warning
- This occurs when multiple models have the same slug
- Solution: Ensure unique slugs within each model type
- The command skips duplicates to maintain data integrity
-
Not all URLs are generated
- Check for duplicate slugs in your data
- Verify models have unique slug values
- For large datasets, increase PHP memory limit
- Run the command multiple times if needed
-
Models using 'active' instead of 'is_active'
- Override the
activeUrlField()
method in your model:
public function activeUrlField(): string { return 'active'; // Your model's active field name }
- Override the
Sitemap not accessible
Check that:
- Sitemap generation is enabled in config
- The public directory is writable
- Routes are properly registered
Redirects not working
Verify:
- The fallback route is registered last in
routes/web.php
- No other routes are conflicting
- Redirect depth limit hasn't been exceeded
Contributing
Contributions are welcome! Please submit pull requests with tests.
License
MIT License. See LICENSE file for details.
Support
For issues and questions, please use the GitHub issue tracker.
Credits
Created by Kiran Timsina at RayzenAI.