Defining routes for Extbase plugins
Each entry in the
routes
list of the
Extbase plugin enhancer maps one
controller/action combination to a URL pattern. For an incoming request, TYPO3
tests the entries in order, and the first one that fits wins. To generate a
URL, TYPO3 sorts the entries first, see
How route variants are matched.
See also
Route Enhancements and Aspects — route configuration keys —
full reference for
defaults
,
requirements
,
static
, and
_arguments
.
Anatomy of a route entry
routes:
- routePath: '/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
routePath - The URL segment appended to the page slug. Static text and
{placeholder}variables can be combined freely. A bare/matches the page URL with nothing appended — useful for the default action. _controller- The controller/action pair in
Controllerformat — noName:: action Name Actionsuffix, no namespace. This tells TYPO3 which route to pick when generating a URL for that action, and which action to dispatch to when resolving an incoming request. _arguments-
Maps placeholder names in
routeto Extbase argument names. In the example above, the URL segment captured byPath {conference_is passed to the action as the argument namedslug} conference. The action receives the raw URL value — typically a slug string liketypo3camp-. An aspect on2025 conference_then translates that slug into the UID that Extbase uses to load the object. Without an aspect the raw string reaches the action unchanged.slug Omit
_argumentsentirely when the placeholder name already matches the Extbase argument name.
Route order and specificity
For an incoming URL, routes are evaluated top to bottom, and the first route
that fits wins. A route does not fit if the URL does not match its
route and
requirements
, or if one of its
aspects cannot resolve the value taken from the
URL. TYPO3 then tries the next route.
A placeholder without an aspect and without a requirement accepts every value of its URL segment. A route made of such a placeholder swallows the requests meant for the routes after it.
Consider a plugin with a list action, a paginated list, and a detail action.
Given the page slug /conferences, the following URLs need to resolve
correctly:
/conferences/→Conference::(no arguments)list /conferences/→page- 2 Conference::withlist currentPage = 2 /conferences/→typo3camp- 2025 Conference::with the resolved UIDshow
With this route order, resolution works correctly:
routes:
- routePath: '/'
_controller: 'Conference::list'
- routePath: '/page-{page}'
_controller: 'Conference::list'
_arguments:
page: currentPage
- routePath: '/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
If
/ had no aspect and were listed before
/page-, the request for /conferences/ would match
it.
conference_ would receive the value page-, and the
pagination route would never be reached.
With a PersistedAliasMapper
on
conference_, as in the complete examples, the order is no longer
decisive for this URL: the mapper finds no conference with the slug
page-, so TYPO3 skips the route and the pagination route matches.
Put routes with a static segment first all the same:
- TYPO3 compares the static segment as a string, which rejects a URL without
any further work. A
Persistedqueries the database for every URL it is asked to resolve.Alias Mapper - If a value is valid for both routes, for example a conference with the slug
page-, the route listed first wins.2
See also
The wrong route is used — troubleshooting routes that fit the same URL.
Separate routes instead of defaults
The
defaults
key at enhancer level makes a placeholder optional: an
incoming URL may leave it out, and TYPO3 fills in the default value. When
generating a URL, TYPO3 leaves out a placeholder whose value is the default,
but only the placeholder. The static text of the route path stays in the URL.
With
defaults
set to
page: '1'
, the route
/page- therefore generates /conferences/ for the
first page. TYPO3 tries a route with placeholders before the route
/
,
see How route variants are matched.
So even a link to the list without any page argument gets
/conferences/ instead of /conferences/.
A default value also reaches the action. With
month: ''
as default, a
request for /conferences/ passes an empty month to the action.
Configure one route for each combination of arguments instead, and leave out
defaults
:
routeEnhancers:
ConferencesPlugin:
type: Extbase
limitToPages:
- 'page["module"] == "conferences"'
extension: MyExtension
plugin: Conferences
defaultController: 'Conference::list'
routes:
- routePath: '/'
_controller: 'Conference::list'
- routePath: '/page-{page}'
_controller: 'Conference::list'
_arguments:
page: currentPage
- routePath: '/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
requirements:
page: '\d+'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '100'
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
- A link to the list without
currentonly fits the routePage /, because/page-needs a value for its placeholder. It generates{page} /conferences/. - A link with
currentset toPage 2generates/conferences/.page- 2
A link with
current set to 1 generates
/conferences/, a second URL for the first page. The pagination
template could leave out the argument for the first page. The action then
uses the default value of its parameter:
<nav aria-label="Pagination">
<f:for each="{pagination.allPageNumbers}" as="page">
<f:if condition="{page} == 1">
<f:then>
<f:link.action action="list">{page}</f:link.action>
</f:then>
<f:else>
<f:link.action action="list" arguments="{currentPage: page}">
{page}
</f:link.action>
</f:else>
</f:if>
</f:for>
</nav>
See also
The wrong route is used —
how
defaults
make a route fit a link that does not supply all of
its arguments.
Constraining placeholders with requirements
requirements
defines a regular expression per placeholder. A route only
matches an incoming URL segment if it satisfies the requirement; without a
requirement, any string matches.
In the full example below,
page: '\ limits the pagination route to
digits: /conferences/ fits it, /conferences/ does
not.
A requirement alone does not remove c from the generated URL, however
strict it is. In the example, the
StaticRangeMapper makes
page
static. A placeholder without an aspect needs a
static
declaration in addition to its requirement, see
The cHash parameter.
A requirement also applies to a placeholder that has an aspect. It is checked against the value in the URL, such as a slug, not against the value the action receives, such as a UID. See Aspects and requirements together.
routeEnhancers:
ConferencesPlugin:
type: Extbase
limitToPages:
- 'page["module"] == "conferences"'
extension: MyExtension
plugin: Conferences
defaultController: 'Conference::list'
routes:
- routePath: '/'
_controller: 'Conference::list'
- routePath: '/page-{page}'
_controller: 'Conference::list'
_arguments:
page: currentPage
- routePath: '/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
requirements:
page: '\d+'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '100'
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
The next step is configuring the
aspects
entries that translate
placeholder values into human-readable URL segments — see
Routing aspects.