anomaly/redirects-module
Anomaly Redirects Module brings URL redirect management to Laravel Streams applications. Create and manage 301/302 redirects in the admin panel and let middleware forward old URLs to new destinations. Integrates with Anomaly field types and the control panel for a simple, configurable redirect syste...
This is not a generic Laravel package. anomaly/redirects-module is an addon for the Streams Platform (PyroCMS 3.x), and it depends on anomaly/streams-platform being present. If you're on a plain Laravel app, stop here — you'd be pulling in an entire CMS kernel to get a redirects table. If you're already running Streams/PyroCMS, this is essentially the only first-party redirect manager, and the 2-star/0-dependent numbers are a reflection of the niche ecosystem, not a defect signal.
Minimum viable path inside a Streams app:
composer require anomaly/redirects-module
php artisan addon:install anomaly/redirects-module
# or: php artisan module:install anomaly/redirects-module
The install runs the addon's migrations and registers its service provider. After that, the admin UI lives at /admin/redirects (check resources/routes/admin.php in the addon — the exact prefix comes from the module's route file, not from convention).
Where to look first, in order:
src/RedirectsModuleServiceProvider.php — what it binds and when.src/RedirectsModule.php — the module's listeners, routes, and any on() hooks.migrations/ — the authoritative schema for the redirects table (fields like from, to, status, secure, domain, query; verify exact names here before writing code against them).src/Http/Controller/Admin/RedirectsController.php — the CRUD surface you'll be extending or imitating.First use case: a client renamed a section of the site. Open /admin/redirects, create a row from = /old-page, to = /new-page, status = 301, and confirm with curl -I https://site.test/old-page. If you get a 200 from the app instead of a 301, the module's catch-all is being shadowed by a real route or isn't running — see the first gotcha below.
Route-ordering is the whole integration. The module works by registering a fallback listener/route that only fires when nothing else matched. In practice that means it must be registered after your application routes. In a Streams app this usually happens for you via the module's service provider, but if you've added a catch-all Route::get('{any}', ...) of your own for a SPA, you've disabled the module without any error message. Audit with:
php artisan route:list | grep -i redirect
Programmatic creation is the pattern you'll use for migrations and imports rather than clicking through the admin UI:
// Verify the exact repository/namespace against src/Repositories/
app(\Anomaly\RedirectsModule\Redirect\RedirectRepositoryInterface::class)->create([
'from' => '/legacy/path',
'to' => '/replacement',
'status' => 301,
]);
Wrap bulk imports (hundreds of legacy URLs from an old CMS export) in a seeder or a one-off artisan command, not in a loop of HTTP calls to the admin endpoint. Import from a CSV of from,to,status and normalize the from column before insert — see gotchas.
Status-code discipline as a workflow. Treat the status column as a first-class decision, not a default:
301 for permanent renames and consolidations (preserves SEO equity).302 for A/B tests, campaign links, and anything you might reverse.A→B, B→C); flatten them to A→C at import time. Chains cost an extra round-trip and the module won't collapse them for you.Query-string and domain matching. The schema typically carries flags for whether the query string and domain are matched. Use them deliberately:
/search with query matching off will happily swallow /search?q=shoes — often what you want, sometimes a privacy leak.Log-then-redirect. A high-leverage pattern for SEO cleanup: pipe your 404 logs into a weekly review, and promote the top-hit 404 paths into redirect rows. The module gives you the storage; the workflow is yours. Keep the redirect list under a few hundred entries and prune quarterly — abandoned redirects accumulate and slow lookups.
Pairing with translation/localization modules. In a Streams stack, redirects are frequently created alongside a locale switch (/en/foo → /fr/foo). Don't encode locale logic into redirect rows; let the locale module own resolution and use redirects only for genuine one-off mappings.
It is not installable outside Streams. Composer will resolve, but the service provider expects the Streams kernel, the addon manifest system, and the module bootstrapping. Installing it into a bare Laravel app produces a broken container binding at best. If you only need redirects, use a standalone package (spatie/laravel-missing-page-redirector or a two-line Route::fallback with a DB lookup) — you'll have far less surface area to debug.
Leading slashes and trailing slashes must be consistent. The most common "the redirect exists but doesn't fire" report is a from stored as old-page while the request path arrives as /old-page. Normalize on write, and add a unique index or validation rule so you can't create near-duplicate rows. Verify the actual comparison logic in the module's listener rather than assuming it trims.
Caching will bite you. If the module or your app caches the lookup table, a freshly created redirect may not fire for minutes. Clear the relevant cache after bulk changes. In Streams, php artisan streams:compile rebuilds the addon's compiled assets/definitions and is the first thing to run when behavior and code disagree.
Loop detection is not a given. Two rows pointing at each other produce an infinite redirect the browser truncates with ERR_TOO_MANY_REDIRECTS. There's no guarantee the module validates against this at write time. Add a check in your import script and a periodic audit query, or at minimum a curl -IL --max-redirs 5 smoke test in CI against a list of known-good URLs.
Permissions and the admin UI. Access to /admin/redirects is governed by Streams role permissions, not a route middleware you can grep for. If a non-admin editor can't see the module, the fix is in the roles UI, not in code. Equally, if the admin panel 404s, the addon is likely installed but not enabled — check php artisan addon:list.
Debugging order when a redirect doesn't fire:
php artisan route:list — is the redirect catch-all registered, and last?streams:compile run?storage/logs/laravel.log — the listener may be throwing silently on a malformed row.Extension point. Because rows are Streams entries, you can attach custom fields to the redirect stream and read them from the model rather than altering the table. That's the sanctioned customization path — prefer it over editing the addon's migrations, which you'll lose on the next composer update. If you do need to override behavior, bind your own implementation of the repository interface in a service provider rather than forking the addon; with 2 stars and 0 dependents, upstream fixes are unlikely, so a thin local override is the sustainable move.
Budget for abandonment. Low stars, no dependents, and a single-purpose MIT addon means you are effectively the maintainer if anything breaks. Pin the version in composer.json, keep a written inventory of every redirect row outside the database (a CSV in the repo), and make sure the site degrades to plain 404s rather than errors if the addon is ever disabled. That export is also your migration path off the module if you leave Streams.
How can I help you explore Laravel packages today?