lindemannrock / craft-redirect-manager
Intelligent redirect management and 404 handling for Craft CMS
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:craft-plugin
pkg:composer/lindemannrock/craft-redirect-manager
Requires
- php: ^8.2
- craftcms/cms: ^5.0.0
- lindemannrock/craft-logging-library: ^5.0
- matomo/device-detector: ^6.4
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/phpstan: dev-main
README
Intelligent redirect management and 404 handling for Craft CMS.
Features
- Automatic 404 Handling - Catches all 404s and attempts to redirect
- Multiple Match Types:
- Exact Match - Case-insensitive exact URL matching
- RegEx Match - Full regular expression support
- Wildcard Match - Simple * wildcards
- Prefix Match - URL starts with pattern
- Rich Analytics - Track 404s with device detection, browsers, OS, geographic data, and bot identification
- Device Detection - Powered by Matomo DeviceDetector for accurate device, browser, and OS identification
- Bot Filtering - Identify and filter bot traffic (GoogleBot, BingBot, etc.)
- Geographic Detection - Track visitor location (country, city) via ip-api.com
- Auto-Redirect Creation - Automatically creates redirects when entry URIs change
- Smart Caching - Fast redirect lookups and device detection with configurable caching
- Auto-Refreshing Dashboard - Configurable auto-refresh (5-60 seconds) with smart pause on user interaction
- CSV Export - Export comprehensive analytics including device and geo data
- Multi-Site Support - Site-specific or global redirects
- Plugin Integration - Pluggable architecture allowing other plugins to integrate 404 handling
- Privacy-First - IP hashing with salt, optional subnet masking, GDPR-friendly
- Logging Integration - Uses logging-library for consistent logs
Requirements
- Craft CMS 5.0 or later
- PHP 8.2 or later
- Logging Library 5.0 or greater (installed automatically as dependency)
- Matomo Device Detector 6.4 or greater (installed automatically as dependency)
Installation
Via Composer
cd /path/to/project
composer require lindemannrock/craft-redirect-manager
./craft plugin/install redirect-manager
Using DDEV
cd /path/to/project
ddev composer require lindemannrock/craft-redirect-manager
ddev craft plugin/install redirect-manager
Via Control Panel
In the Control Panel, go to Settings → Plugins and click "Install" for Redirect Manager.
Important: IP Privacy Protection
Redirect Manager uses privacy-focused IP hashing with a secure salt:
- ✅ Rainbow-table proof - Salted SHA256 prevents pre-computed attacks
- ✅ Unique visitor tracking - Same IP = same hash
- ✅ Maximum privacy - Original IPs never stored, unrecoverable
- ✅ Optional subnet masking - Additional anonymization layer
Setup Instructions:
- Generate salt:
php craft redirect-manager/security/generate-salt - Command automatically adds
REDIRECT_MANAGER_IP_SALTto your.envfile - Manually copy the salt value to staging/production
.envfiles - Never regenerate the salt in production
How It Works:
- Plugin automatically reads salt from
.env(no config file needed!) - Config file can override if needed:
'ipHashSalt' => App::env('REDIRECT_MANAGER_IP_SALT') - If no salt found, error banner shown in settings
Security Notes:
- Never commit the salt to version control
- Store salt securely (password manager recommended)
- Use the SAME salt across all environments (dev/staging/production)
- Changing the salt will break unique visitor tracking history
Local Development: Analytics Location Override
When running locally (DDEV, localhost), analytics will default to Dubai, UAE because local IPs can't be geolocated. To set your actual location for testing:
# Add to your .env file:
REDIRECT_MANAGER_DEFAULT_COUNTRY=US
REDIRECT_MANAGER_DEFAULT_CITY=New York
Supported locations:
- US: New York, Los Angeles, Chicago, San Francisco
- GB: London, Manchester
- AE: Dubai, Abu Dhabi (default: Dubai)
- SA: Riyadh, Jeddah
- DE: Berlin, Munich
- FR: Paris
- CA: Toronto, Vancouver
- AU: Sydney, Melbourne
- JP: Tokyo
- SG: Singapore
- IN: Mumbai, Delhi
Note: This only affects local/private IPs (127.0.0.1, localhost, etc.). Production analytics will use real IP geolocation via ip-api.com.
Configuration
Config File
Create a config/redirect-manager.php file to override default settings:
cp vendor/lindemannrock/craft-redirect-manager/src/config.php config/redirect-manager.php
Example configuration:
<?php return [ // Auto create redirects when entry URIs change 'autoCreateRedirects' => true, // Analytics (master switch - controls device detection, geo, IP tracking) 'enableAnalytics' => true, 'enableGeoDetection' => false, // Track visitor location 'anonymizeIpAddress' => false, // Subnet masking for privacy // Analytics retention in days (0 = keep forever) 'analyticsRetention' => 30, 'analyticsLimit' => 1000, // Dashboard 'refreshIntervalSecs' => 5, // Auto-refresh interval (5, 15, 30, or 60 seconds) // Performance & Caching 'enableRedirectCache' => true, 'redirectCacheDuration' => 3600, // 1 hour 'cacheDeviceDetection' => true, 'deviceDetectionCacheDuration' => 3600, // 1 hour // Query String Handling 'stripQueryString' => false, // Strip query string for redirect matching 'preserveQueryString' => false, // Pass query string to destination URL 'stripQueryStringFromStats' => true, // Group analytics by path only // Log level 'logLevel' => 'error', ];
See Configuration Documentation for all available options.
Environment-Specific Config
<?php return [ '*' => [ 'autoCreateRedirects' => true, ], 'production' => [ 'logLevel' => 'error', 'analyticsRetention' => 90, ], 'dev' => [ 'logLevel' => 'debug', 'analyticsRetention' => 7, ], ];
Usage
Creating Redirects
Via Control Panel:
- Navigate to Redirect Manager → Redirects
- Click New redirect
- Fill in:
- Source URL:
/old-pageor/blog/*or regex pattern - Destination URL:
/new-pageor absolute URL - Match Type: exact, regex, wildcard, or prefix
- Status Code: 301 (permanent) or 302 (temporary)
- Source URL:
- Save
Programmatically:
use lindemannrock\redirectmanager\RedirectManager; RedirectManager::$plugin->redirects->createRedirect([ 'sourceUrl' => '/old-page', 'destinationUrl' => '/new-page', 'matchType' => 'exact', 'statusCode' => 301, 'enabled' => true, 'priority' => 0, 'siteId' => null, // null = all sites ]);
Match Type Examples
Exact Match:
Source: /old-page
Matches: /old-page (case-insensitive)
Wildcard Match:
Source: /blog/*
Matches: /blog/post-1, /blog/category/news, etc.
Prefix Match:
Source: /old-
Matches: /old-page, /old-blog, /old-anything
RegEx Match:
Source: ^/blog/(\d+)/(.*)$
Destination: /article/$1/$2
Viewing Analytics
Dashboard:
- Navigate to Redirect Manager → Analytics
- View handled vs unhandled 404s
- See most common 404s
- Create redirects from unhandled 404s with one click
Export CSV:
Redirect Manager → Analytics → Export CSV
Automatic Entry Redirects
When enabled (default), the plugin automatically creates redirects when:
- Entry slug changes:
/old-slug→/new-slug - Entry moves in structure:
/parent/child→/new-parent/child
Disable in settings: Redirect Manager → Settings → Auto Create Redirects
Query String Handling
Redirect Manager provides three settings to control how query strings are handled at different stages:
1. Strip Query String (Redirect Matching)
Controls whether query strings affect redirect matching.
OFF (default): Exact matching required
- Redirect rule:
/page /page✅ matches/page?foo=bar❌ does not match
ON: Query strings ignored during matching
- Redirect rule:
/page /page✅ matches/page?foo=bar✅ matches
2. Preserve Query String (Redirect Destination)
Controls whether query strings are passed to the destination URL.
OFF (default): Query strings dropped
- User visits:
/old?ref=email - Redirects to:
/new
ON: Query strings preserved
- User visits:
/old?ref=email - Redirects to:
/new?ref=email
3. Strip Query String From Stats (Analytics Grouping)
Controls how analytics groups URLs with different query strings.
ON (default): Consolidate by path
- Dashboard shows:
/page?source=google(count: 3) - Hits:
/page?source=email,/page?source=facebook,/page?source=google - All grouped into one record, displays latest query string
OFF: Separate records per unique URL+query
- Dashboard shows: 3 separate rows, each with count: 1
/page?source=email,/page?source=facebook,/page?source=google
Common Configurations
E-commerce/Marketing Sites:
'stripQueryString' => true, // Match any UTM params 'preserveQueryString' => true, // Keep tracking through redirects 'stripQueryStringFromStats' => true, // Consolidate analytics reports
API/Applications:
'stripQueryString' => false, // Exact URL matching required 'preserveQueryString' => false, // Canonical URLs without params 'stripQueryStringFromStats' => false, // Track each unique combination
Note: Analytics always store paths (e.g., /page?foo=bar), never full URLs with domains.
Testing Guide
1. Test Basic Redirect
# Create a test redirect via CP: # Source: /test-redirect # Destination: / # Match Type: exact # Status Code: 301 # Test it: curl -I http://your-site.test/test-redirect # Should return: HTTP/1.1 301 Moved Permanently # Location: http://your-site.test/
2. Test 404 Tracking
# Visit a non-existent page: curl -I http://your-site.test/page-that-does-not-exist # Check analytics: # CP → Redirect Manager → Analytics # Should show the 404 with "Handled: No"
3. Test Auto-Redirect Creation
- Create a test entry with slug
test-entry - Note the URL (e.g.,
/test-entry) - Change the slug to
renamed-entry - Save the entry
- Check Redirect Manager → Redirects
- Should see auto-created redirect:
/test-entry→/renamed-entry
4. Test Wildcard Matching
# Create redirect: # Source: /old-blog/* # Destination: /blog/ # Match Type: wildcard curl -I http://your-site.test/old-blog/any-post # Should redirect to /blog/
5. Test Analytics Cleanup
# Run cleanup job manually: php craft queue/run # Or test the service directly: php craft console/controller/eval \ "echo lindemannrock\redirectmanager\RedirectManager::\$plugin->analytics->cleanupOldAnalytics();"
Permissions
- View redirects - View redirect list
- Create redirects - Create new redirects
- Edit redirects - Modify existing redirects
- Delete redirects - Remove redirects
- View analytics - Access 404 analytics
- View logs - Access plugin logs
- Manage settings - Change plugin settings
Logging
Redirect Manager uses the LindemannRock Logging Library for centralized logging.
Log Levels
- Error: Critical errors only (default)
- Warning: Errors and warnings
- Info: General information
- Debug: Detailed debugging (includes performance metrics, requires devMode)
Configuration
// config/redirect-manager.php return [ 'logLevel' => 'error', // error, warning, info, or debug ];
Note: Debug level requires Craft's devMode to be enabled. If set to debug with devMode disabled, it automatically falls back to info level.
Log Files
- Location:
storage/logs/redirect-manager-YYYY-MM-DD.log - Retention: 30 days (automatic cleanup via Logging Library)
- Format: Structured JSON logs with context data
- Web Interface: View and filter logs in CP at Redirect Manager → Logs
Log Management
Access logs through the Control Panel:
- Navigate to Redirect Manager → Logs
- Filter by date, level, or search terms
- Download log files for external analysis
- View file sizes and entry counts
- Auto-cleanup after 30 days (configurable via Logging Library)
Requires: lindemannrock/craft-logging-library plugin (installed automatically as dependency)
See docs/LOGGING.md for detailed logging documentation.
Troubleshooting
Redirects Not Working
-
Check plugin is installed:
php craft plugin/list
-
Check database tables exist:
php craft migrate/all --plugin=redirect-manager
-
Check logs:
CP → Redirect Manager → Logs -
Enable debug logging:
// config/redirect-manager.php return [ 'logLevel' => 'debug', ];
-
Clear caches:
php craft clear-caches/all
Analytics Not Recording
- Check Settings → Analytics → Enable Analytics is enabled (master switch)
- Check analytics limit hasn't been reached
- Ensure IP hash salt is configured (run:
php craft redirect-manager/security/generate-salt) - Check database:
SELECT * FROM redirectmanager_analytics
Auto-Redirects Not Creating
- Check Settings → General → Auto Create Redirects is enabled
- Check entry has a URI (not disabled, not in a disabled section)
- Check logs for any errors
Events
Listen to redirect events in your own plugins:
use lindemannrock\redirectmanager\services\RedirectsService; use lindemannrock\redirectmanager\events\RedirectEvent; use yii\base\Event; Event::on( RedirectsService::class, RedirectsService::EVENT_BEFORE_SAVE_REDIRECT, function(RedirectEvent $event) { // Modify redirect before saving $event->redirect['statusCode'] = 302; // Or prevent saving // $event->isValid = false; } );
Available events:
EVENT_BEFORE_SAVE_REDIRECTEVENT_AFTER_SAVE_REDIRECTEVENT_BEFORE_DELETE_REDIRECTEVENT_AFTER_DELETE_REDIRECT
Plugin Integration
Redirect Manager provides a pluggable architecture that allows other plugins to integrate their 404 handling, similar to how plugins use the Logging Library.
RedirectHandlingTrait - What It Provides
The trait provides 2 methods that other plugins can use:
| Method | Returns | Description |
|---|---|---|
handleRedirect404(string $url, string $source, array $context) |
?array |
Check if a redirect exists for a 404 URL |
createRedirectRule(array $attributes) |
bool |
Create a new redirect in Redirect Manager |
Important: The trait does NOT provide handler functions like handleDeletedItem() or handle404(). Those are examples of functions you write in your own plugin that call the trait's methods.
Integration Method 1: Handle 404s
When your plugin encounters a 404, check if Redirect Manager has a matching redirect.
Example implementation (you write this code):
use lindemannrock\redirectmanager\traits\RedirectHandlingTrait; class MyController extends Controller { use RedirectHandlingTrait; /** * Your custom 404 handler (not provided by trait) */ private function handle404(): Response { $url = Craft::$app->getRequest()->getUrl(); // Call the trait's handleRedirect404() method $redirect = $this->handleRedirect404($url, 'my-plugin', [ 'type' => 'custom-404', 'context' => 'additional-metadata' ]); if ($redirect) { // Redirect found! Use it return $this->redirect($redirect['destinationUrl'], $redirect['statusCode']); } // No redirect found, use your fallback return $this->redirect('/', 302); } }
What happens:
- Your plugin encounters a 404 (e.g.,
/my-plugin/xyzdoesn't exist) - You call
handleRedirect404()to check if a redirect exists - Redirect Manager searches for matching redirects
- Analytics are recorded with your plugin as the source
- Returns redirect data if found, or
nullif not
Integration Method 2: Push Redirects
Your plugin can automatically create redirects when certain events occur (item deleted, slug changed, link expired, etc.).
Example implementation (you write this code):
use lindemannrock\redirectmanager\traits\RedirectHandlingTrait; class MyService extends Component { use RedirectHandlingTrait; /** * Your custom deletion handler (not provided by trait) * * This is example code showing how YOU would implement auto-redirect creation. * The function name "handleDeletedItem" is just an example - name it whatever you want. */ public function handleDeletedItem($item): void { // Your business logic here if ($item->hits === 0) { return; // Don't create redirect for unused items } // Call the trait's createRedirectRule() method $this->createRedirectRule([ 'sourceUrl' => '/items/' . $item->slug, 'sourceUrlParsed' => '/items/' . $item->slug, 'destinationUrl' => '/items', 'matchType' => 'exact', // exact|regex|wildcard|prefix 'redirectSrcMatch' => 'pathonly', // pathonly|fullurl (REQUIRED) 'statusCode' => 301, 'siteId' => $item->siteId, 'enabled' => true, 'priority' => 0, 'creationType' => 'item-deleted', // What happened (max 50 chars) 'sourcePlugin' => 'my-plugin', // Your plugin handle in kebab-case (max 50 chars) ], true); // Show notification to user } }
Slug Change Example with Undo Detection:
use lindemannrock\redirectmanager\traits\RedirectHandlingTrait; class MyService extends Component { use RedirectHandlingTrait; public function handleSlugChange(string $oldSlug, MyElement $element): void { $oldUrl = '/items/' . $oldSlug; $newUrl = '/items/' . $element->slug; // Check for immediate undo (A→B→A within time window) if ($this->handleUndoRedirect($oldUrl, $newUrl, $element->siteId, 'item-slug-change', 'my-plugin')) { return; // Undo was detected and handled } // Create redirect with notification $this->createRedirectRule([ 'sourceUrl' => $oldUrl, 'sourceUrlParsed' => $oldUrl, 'destinationUrl' => $newUrl, 'matchType' => 'exact', 'redirectSrcMatch' => 'pathonly', 'statusCode' => 301, 'siteId' => $element->siteId, 'enabled' => true, 'priority' => 0, 'creationType' => 'item-slug-change', 'sourcePlugin' => 'my-plugin', ], true); // Show notification } }
Trait Methods:
createRedirectRule(array $attributes, bool $showNotification = false): bool
- Creates a redirect in Redirect Manager
$showNotification- Set totrueto show "Redirect created: X → Y" notification to user- Returns
trueon success,falseon failure
handleUndoRedirect(string $oldUrl, string $newUrl, int $siteId, string $creationType, string $sourcePlugin): bool
- Detects and handles immediate undo (flip-flop redirects within undo window)
- Example: Change A→B, then quickly change B→A - deletes the A→B redirect instead of creating B→A
- Respects Redirect Manager's "Undo Window" setting (30-240 minutes)
- Shows "Slug change undone - previous redirect removed." notification
- Returns
trueif undo was handled,falseotherwise
handleRedirect404(string $url, string $source, array $context = []): ?array
- Checks if Redirect Manager has a redirect for a 404 URL
- Returns redirect data if found,
nullotherwise
Important Constraints:
creationType- Maximum 50 characters (e.g.,'code-change','item-deleted','smart-link-expired')sourcePlugin- Maximum 50 characters, always kebab-case (e.g.,'shortlink-manager','smart-links','my-plugin')redirectSrcMatch- REQUIRED - Must be'pathonly'or'fullurl'elementId- (Optional) Element ID if redirect was created by element URI change. Used to track and clean up redirect chains.sourceUrl/sourceUrlParsed- Maximum 255 charactersdestinationUrl- Maximum 500 characters
Plugin Handle Format:
- ✅
'shortlink-manager'(correct) - ✅
'smart-links'(correct) - ❌
'ShortLink Manager'(wrong - never use title case) - ❌
'shortlink_manager'(wrong - use kebab-case, not snake_case)
What happens:
- Your plugin detects an event (deletion, slug change, etc.)
- You call
handleUndoRedirect()to check for immediate undo - If no undo, call
createRedirectRule()to create the redirect - Redirect Manager validates, saves, and shows notification (if enabled)
- Cache is invalidated
- Future requests to the old URL will be redirected
Source Plugin Tracking
All 404s tracked through external plugins are recorded with their source plugin identifier:
// The second parameter ('shortlink-manager') becomes the source $redirect = $this->handleRedirect404($url, 'shortlink-manager', [ 'type' => 'shortlink-not-found', 'code' => 'abc123' ]);
Analytics dashboard shows breakdown by source:
Redirect Manager: 145 (handled: 98)
ShortLink Manager: 67 (handled: 54)
Smart Links: 43 (handled: 38)
This helps you identify which plugin or area is generating the most 404s.
Real-World Example: ShortLink Manager
See how ShortLink Manager integrates:
404 Handling: RedirectController::redirectToNotFound()
- Calls
handleRedirect404()when shortlink not found - Executes redirect if found
- Falls back to configured URL if not
Auto-Redirect Creation: ShortLinksService
handleCodeChange()- Creates redirect when shortlink code changeshandleExpiredShortLink()- Creates redirect when shortlink expireshandleDeletedShortLink()- Creates redirect when shortlink deleted (if has traffic)
Benefits
✅ Centralized 404 Tracking - See all 404s across your entire site in one dashboard ✅ Auto-Healing - Broken links automatically fixed when redirects exist ✅ Source Attribution - Know which plugin or area is generating 404s ✅ Loose Coupling - Plugins work independently, integration is optional ✅ Easy Integration - Add trait, write your handlers, call the methods, done!
API
Services
use lindemannrock\redirectmanager\RedirectManager; // Redirects $plugin = RedirectManager::$plugin; $plugin->redirects->createRedirect([...]); $plugin->redirects->updateRedirect($id, [...]); $plugin->redirects->deleteRedirect($id); $plugin->redirects->findRedirect($fullUrl, $pathOnly); $plugin->redirects->handleExternal404($url, $context); // For plugin integration // Analytics $plugin->analytics->record404($url, $handled, $context); // Context tracks source plugin $plugin->analytics->getAllAnalytics($siteId, $limit); $plugin->analytics->getChartData($siteId, $days); $plugin->analytics->getDeviceBreakdown($siteId, $days); $plugin->analytics->getBrowserBreakdown($siteId, $days); $plugin->analytics->getOsBreakdown($siteId, $days); $plugin->analytics->getBotStats($siteId, $days); $plugin->analytics->getLocationFromIp($ip); $plugin->analytics->exportToCsv($siteId); // Device Detection $plugin->deviceDetection->detectDevice($userAgent); $plugin->deviceDetection->isMobileDevice($deviceInfo); $plugin->deviceDetection->isBot($deviceInfo); // Matching $plugin->matching->matches($matchType, $pattern, $url);
Support
- Documentation: https://github.com/LindemannRock/craft-redirect-manager
- Issues: https://github.com/LindemannRock/craft-redirect-manager/issues
- Email: support@lindemannrock.com
License
This plugin is licensed under the MIT License. See LICENSE for details.
Developed by LindemannRock