Skip to content
Back to skills

Nette Routing

ASecurity

Invoke before writing or modifying a router, a RouterFactory or any URL mask in Nette. Use for RouteList, addRoute(), route masks (optional `[...]` parts, `<param regex>` constraints, defaults in the mask), withModule()/withDomain()/withPath(), pretty URLs and slugs, one-way routes for retired URLs, Route::FilterIn/FilterOut/FilterTable/FilterStrict, general filters under the `''` key, fixed parameters, SimpleRouter, the `routing:` NEON section, and canonical-URL redirects (canonicalize(), $a...

  • 44 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
developmentgophpdebuggingdocumentation

Works with

  • cli

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add nette/claude-code --skill nette-routing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nette Routing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nette Routing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nette-nette-routing/badge)](https://www.skillsdirectory.com/skills/nette-nette-routing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nette-routing
description: Invoke before writing or modifying a router, a RouterFactory or any URL mask in Nette. Use for RouteList, addRoute(), route masks (optional `[...]` parts, `<param regex>` constraints, defaults in the mask), withModule()/withDomain()/withPath(), pretty URLs and slugs, one-way routes for retired URLs, Route::FilterIn/FilterOut/FilterTable/FilterStrict, general filters under the `''` key, fixed parameters, SimpleRouter, the `routing:` NEON section, and canonical-URL redirects (canonicalize(), $autoCanonicalize). Also trigger for "wrong URL is generated", "link points to the wrong route", dots in URLs of a modular app, unexpected 301 redirect loops, or the Tracy routing panel. Not for `n:href`/`{link}` syntax (latte-templates) nor signal links (nette-components).
---

## Nette Routing

A route is **bidirectional**: the same object matches an incoming URL and generates links, and almost
every routing bug comes from forgetting the second direction. Related skills: **nette-architecture**
(`RouterFactory`, lifecycle), **nette-configuration** (registering it), **nette-components** (signal
links), **latte-templates** (`n:href`/`{link}`). Verified against nette/application 3.3.0 and
nette/routing 3.1.3.

### Order governs link GENERATION, not just matching

`RouteList::constructUrl()` walks the routes in declaration order and returns the URL from **the first
route that can build one** – so a catch-all placed first silently hijacks every link, with no error:

```php
$router->addRoute('admin/<presenter>/<action>', 'Admin:Default:default'); // WRONG order:
$router->addRoute('rss.xml', 'Feed:rss');       // link to Feed:rss generates '/admin/feed/rss'
```

Specific first, catch-all last. The **canonical URL of a page is whatever the first non-one-way matching
route generates**, so the order also decides where canonicalization redirects.

### Mask grammar

```php
$router->addRoute('chronicle/<year=2020>', 'History:show');           // default inside the mask
$router->addRoute('<presenter>/<action>[/<id \d+>]', 'Home:default'); // optional part + regex
$router->addRoute('<path .+>', 'File:serve');   // default pattern is [^/]+; only .+ eats slashes
$router->addRoute('//<lang>.example.com/<presenter>', /* ... */);     // host part needs the // prefix
```

- **A defaulted parameter becomes optional only if everything after it is optional too**:
  `'<presenter=Home>/<action=default>/<id=>'` equals
  `'[<presenter=Home>/[<action=default>/[<id>]]]'`, hence the trailing slash. Write the brackets
  yourself – `'[<presenter=Home>[/<action=default>[/<id>]]]'` – to drop it.
  Generation prefers the **shortest** variant; `[!...]` keeps an optional part in generated URLs while
  still accepting URLs without it (`'<name>[!.html]'` generates `/hello.html`).
- **`%basePath%`, `%host%`, `%domain%`, `%sld%`, `%tld%` are substituted only in `//`-masks**, and
  `%basePath%` is matched with its surrounding slashes – write `//www.%domain%/%basePath%/<presenter>`.
- Query parameters can be renamed but not validated: in `'product ? id=<productId>'` a regex would be
  **silently discarded**.
- A parameter in the metadata array but **not** in the mask is constant: unchangeable from the query
  string, and a link passing a different value makes the route decline and fall through – that is how
  `addRoute('tos', ['presenter' => 'Article', 'id' => 123])` works.

### RouteList, modules, nesting

```php
$router = new Nette\Application\Routers\RouteList;
$router->withModule('Front')
	->addRoute('rss.xml', 'Feed:rss')            // presenter becomes Front:Feed
	->addRoute('<presenter>/<action>', 'Home:default')
	->end()
	->withDomain('admin.%domain%')->withModule('Admin')
		->addRoute('<presenter>/<action>', 'Dashboard:default');
return $router;                                  // the ROOT, not the chain result
```

- `withModule()`/`withDomain()`/`withPath()` create and attach a **new child list and return it**, not
  `$this`; `end()` returns the parent. Keep the root in a variable –
  `return (new RouteList)->withModule('Front')->addRoute(...)` returns the child and loses the siblings.
- **Without `withModule()` (or a fixed `module` parameter) the full presenter name lands in the URL**,
  where each `:` becomes a dot and each PascalCase boundary a dash: `Front:Admin:ProductList` shows up as
  `front.admin.product-list`. Dotted URLs mean a missing `withModule()`.
  `<module>` in a mask captures the **whole** module path, up to the *last* colon, and
  `Nette:` presenters (`Nette:Micro`, `Nette:Error`) are never prefixed.
- `$router[] = new Route(...)` is **deprecated** and emits `E_USER_DEPRECATED` – use
  `addRoute()`/`add()`, which return `static`; **`prepend()` returns `void`**. Add a
  `Nette\Application\Routers\Route`, never a `Nette\Routing\Route` – only the former knows
  presenter/action/module inflection (importing the latter for `Route::Value` constants is fine).

### One-way routes

```php
$router->addRoute('product-info', 'Product:detail', oneWay: true);  // old URL, accepted only
$router->addRoute('product/<id>', 'Product:detail');                // canonical, generates links
```

`oneWay` is `int|bool`. One-way routes are skipped while the generation cache is built, so such a route
**can never produce a URL**; the old address is matched and then 301-redirected by canonicalization. `Router::ONE_WAY` still
works but is `@deprecated` on master – prefer the named argument.

### Filters

`Route::FilterIn` converts URL -> application, `FilterOut` back; both take either one parameter (a
string) or, under the empty key `''`, the whole parameter array. `Route::FilterTable` is a declarative
`url => app` map for one parameter and `Route::FilterStrict => true` makes it reject unlisted values.

```php
$router->addRoute('article/<id [0-9]+>[-<slug>]', [
	'presenter' => 'Article', 'action' => 'detail',
	'' => [ // general filter: sees all parameters at once
		Route::FilterOut => fn(array $p): array => $p + ['slug' => $slugs->get((int) $p['id'])],
	],
]);
```

- **The order is asymmetric**: on input the per-parameter `FilterIn` runs *before* the general one, on
  output the general `FilterOut` runs *before* the per-parameter one, so inside a general filter
  `presenter`/`action` are always PascalCase/camelCase.
- `presenter`, `action` and `module` already carry filters converting PascalCase/camelCase to
  kebab-case; write defaults in the **application** form, `<presenter=ProductEdit>`, not
  `<presenter=product-edit>`.
- A `FilterIn` returning `null` **rejects the route** (matching falls through to the next one) unless the
  parameter has a default; `FilterStrict` behaves the same.
- Several `FilterTable` keys may map to one value; the **last** wins as canonical, because the reverse
  table is `array_flip()`ped. Adding a slug to every link is likewise a
  `FilterOut`-only job – no template changes; cache the lookup, one page generates the same link often.

### Canonicalization

`Presenter::canonicalize()` regenerates the URL through the router and 301-redirects if it differs from
the current one. It runs **after the action phase and before signals** (see **nette-architecture** for
the full lifecycle) – an action can still change parameters before it fires, a signal handler cannot.

- Skipped for AJAX and for anything other than GET/HEAD. Disable it with
  `$this->autoCanonicalize = false` and call `$this->canonicalize()` where it suits; `switch()` inside an
  action disables it automatically.
- 301 unless the request carries the `VARYING` flag, which gives 302; nothing in the framework sets it – use `$this->getRequest()->setFlag(Nette\Application\Request::VARYING)` when
  the canonical URL depends on the user or locale.
- A redirect loop means the generated URL never equals the incoming one, usually a `FilterOut` that is
  not the exact inverse of what the mask accepts.

### Configuration and debugging

- Register the factory as a service (`- App\Core\RouterFactory::createRouter`), or for query-string URLs
  (`?presenter=…&action=…`) just `- Nette\Application\Routers\SimpleRouter('Home:default')`.
  `routing: cache: true` serializes the compiled router into the DI container, but only works when the
  factory takes **no arguments** and every route is serializable — a closure in `FilterIn`/`FilterOut`
  (the slug pattern above) makes compilation fail with `Unable to cache router due to error:
  Serialization of 'Closure' is not allowed`. Use `FilterTable` or a callable array instead, or leave
  the cache off.
- The Tracy **routing panel** lists every route: green ✓ matched, blue ≈ would also have matched but was
  overtaken, plus a one-way label. After an unexpected redirect read its
  *redirect* bar – it shows how the router understood the original URL. Disable the browser cache; 301s
  are cached aggressively.
- `$presenter->getHttpRequest()->getUrl()` is the **raw** incoming URL, `$presenter->getRequest()` is the
  `Nette\Application\Request` the router produced – comparing them says at once whether the fault is in
  matching or in link generation.

### Online Documentation

For details, see the official documentation:

- [Routing](https://doc.nette.org/en/application/routing) – masks, filters, modules, custom routers
- [Creating Links](https://doc.nette.org/en/application/creating-links) – link syntax, LinkGenerator
- [Pretty URLs with Slugs](https://doc.nette.org/en/best-practices/pretty-urls) – the FilterOut slug pattern
- [Presenters](https://doc.nette.org/en/application/presenters) – canonization, lifecycle
- [Application Configuration](https://doc.nette.org/en/application/configuration) – the `routing:` section

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…