Routers and URL Resolution
Magento tries routers in sort order until one matches; the standard router maps front name, controller and action to one class.
public function match(\Magento\Framework\App\RequestInterface $request) // returns ActionInterface or nullA router is a class with one method, match(), which either returns an action object for the request or returns null so the next router can try. Magento holds the routers in an ordered list, so URL resolution is a chain of attempts rather than one table of patterns.
The storefront chain
| Order | Name | Class | Matches |
|---|---|---|---|
| 10 | robots | Magento\Robots\Controller\Router | /robots.txt |
| 20 | urlrewrite | Magento\UrlRewrite\Controller\Router | a row in url_rewrite, such as a product or category URL |
| 30 | standard | Magento\Framework\App\Router\Base | front name / controller / action |
| 60 | cms | Magento\Cms\Controller\Router | a CMS page identifier |
| 100 | default | Magento\Framework\App\Router\DefaultRouter | everything; hands over to the no-route action |
In adminhtml the chain is two entries: Magento\Backend\App\Router at 10 and the default router at 100.
The standard router
It splits the path into front name, controller path and action, defaulting the last two to index. The front name comes from etc/frontend/routes.xml; the class is Vendor\Module\Controller\ControllerPath\Action. So /blog/post/view/id/3 resolves to Vendor\Blog\Controller\Post\View with id as a request parameter. Admin URLs put the admin front name first and the class lives under Controller/Adminhtml/.
What trips people up
Two modules may claim one front name; the standard router tries them in declared order and takes the first with a matching class. A controller folder named Adminhtml is deliberately unreachable from the standard router, so a storefront route cannot borrow an admin controller.
Example
<?php
// app/code/Vendor/ShortLink/Controller/Router.php
declare(strict_types=1);
namespace Vendor\ShortLink\Controller;
use Magento\Framework\App\Action\Forward;
use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\RouterInterface;
class Router implements RouterInterface
{
public function __construct(
private readonly ActionFactory $actionFactory
) {
}
public function match(RequestInterface $request): ?ActionInterface
{
if (!$request instanceof HttpRequest) {
return null;
}
$path = trim((string)$request->getPathInfo(), '/');
if (!preg_match('#^t/(\d+)$#', $path, $matches)) {
return null;
}
$request->setModuleName('catalog')
->setControllerName('product')
->setActionName('view')
->setParam('id', $matches[1]);
return $this->actionFactory->create(Forward::class);
}
}When to use it
- Serve a short link such as /t/123 without inventing a new front name.
- Work out why a pretty URL reaches a product page and never gets to your controller.
- Decide between a URL rewrite row and a custom router for a set of legacy URLs.
- Find which class answers a given admin URL.
More examples
Joining the router list
Array arguments merge rather than replace, so you add one item instead of restating the whole list. 25 places the router after urlrewrite and before standard, which means a real product URL still wins over your pattern. The file sits under etc/frontend, so the router is only in the storefront chain and never runs for admin or REST. Apply it with bin/magento cache:flush; in production mode run bin/magento setup:di:compile first. Setting disable to true on an existing item, such as cms, removes a core router from the chain.
<?xml version="1.0"?>
<!-- app/code/Vendor/ShortLink/etc/frontend/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="vendor_shortlink" xsi:type="array">
<item name="class" xsi:type="string">Vendor\ShortLink\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">25</item>
</item>
</argument>
</arguments>
</type>
</config>A front name for the standard router
router id="standard" is the storefront router; the route's frontName is the URL segment and the id is the internal route name that appears in the full action name (blog_post_view). With this file in place, /blog resolves to Vendor\Blog\Controller\Index\Index because both missing segments default to index. routes.xml is read into the configuration cache, so run bin/magento cache:flush after adding or editing it, otherwise the URL keeps returning the 404 page.
<?xml version="1.0"?>
<!-- app/code/Vendor/Blog/etc/frontend/routes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="blog" frontName="blog">
<module name="Vendor_Blog"/>
</route>
</router>
</config>The same file for the admin
The admin router expects four segments, admin front name first, so this route answers <admin>/vendor_blog/post/index and loads Vendor\Blog\Controller\Adminhtml\Post\Index. The admin front name is not fixed: it comes from backend/frontName in app/etc/env.php and defaults to admin. before and after order the modules that answer one route, which is how core modules insert their controllers ahead of Magento_Backend's fallbacks. Apply with bin/magento cache:flush.
<?xml version="1.0"?>
<!-- app/code/Vendor/Blog/etc/adminhtml/routes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="vendor_blog" frontName="vendor_blog">
<module name="Vendor_Blog" before="Magento_Backend"/>
</route>
</router>
</config>
Discussion