Routing examples and common mistakes
The following examples build on the concepts explained in the preceding pages. Each is a complete, working configuration you can adapt directly.
On this page
List, pagination, and detail
The most common Extbase plugin pattern: a list page with pagination and a detail view. The list and detail actions are on the same page here; for separate pages see List and detail on separate pages.
URLs produced:
/conferences/— list, first page/conferences/— list, page 2page- 2 /conferences/— detail viewtypo3camp- 2025
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
defaults:
page: '1'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '100'
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
routeValuePrefix: '/'
fallbackValue: null
Key points:
- The pagination route
/page-uses a static prefix{page} page-to distinguish it from the detail route/. Without the prefix,{conference_ slug} /conferences/would match the detail route first and try to resolvepage- 2 page-2as a conference slug. fallbackon theValue: null Persistedmeans a deleted or hidden conference returnsAlias Mapper nullto the action rather than a 404. The action must declare the argument nullable and handle it explicitly — see Handling deleted or hidden records.- Route order matters: pagination before detail, most specific before most general — see Route order and specificity.
List and detail on separate pages
When the list plugin and the detail plugin live on different pages, each page
needs its own enhancer entry —
limit must point to the correct
page for each, and links between them need
set or
page in Fluid.
routeEnhancers:
ConferencesList:
type: Extbase
limitToPages:
- 'page["module"] == "conferences_list"'
extension: MyExtension
plugin: Conferences
defaultController: 'Conference::list'
routes:
- routePath: '/'
_controller: 'Conference::list'
- routePath: '/page-{page}'
_controller: 'Conference::list'
_arguments:
page: currentPage
defaults:
page: '1'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '100'
ConferencesDetail:
type: Extbase
limitToPages:
- 42
extension: MyExtension
plugin: Conferences
defaultController: 'Conference::show'
routes:
- routePath: '/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
aspects:
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
routeValuePrefix: '/'
fallbackValue: null
In the list template, link to the detail page explicitly:
<f:link.action
action="show"
controller="Conference"
arguments="{conference: conference}"
pageUid="{settings.detailPageUid}"
>
{conference.title}
</f:link.action>
Store the detail page UID in TypoScript settings so it is configurable without touching PHP:
plugin.tx_myextension_conferences.settings.detailPageUid = 42
Multiple controllers in one plugin
A single plugin can expose actions from more than one controller — all under the same namespace, all in one enhancer. The key is that every controller/action combination that needs a clean URL gets its own route entry.
URLs produced:
/conferences/— conference list/conferences/— conference detailtypo3camp- 2025 /conferences/— talk list for that conferencetypo3camp- 2025/ talks /conferences/— talk detailtypo3camp- 2025/ talks/ extbase- routing- demystified
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
- routePath: '/{conference_slug}/talks'
_controller: 'Talk::list'
_arguments:
conference_slug: conference
- routePath: '/{conference_slug}/talks/{talk_slug}'
_controller: 'Talk::show'
_arguments:
conference_slug: conference
talk_slug: talk
defaults:
page: '1'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '100'
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
routeValuePrefix: '/'
fallbackValue: null
talk_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_talk
routeFieldName: slug
routeValuePrefix: '/'
fallbackValue: null
The
{conference_ placeholder appears in both the
Conference:: route and the
Talk routes. Each is a separate
route entry with its own
_controller — the enhancer matches on the full
path and controller/action combination, not on the placeholder name alone.
Date-based conference archive
Filter the conference list by year and month using human-readable URL segments. Each combination of arguments needs its own route entry because TYPO3 cannot derive missing arguments from partial matches.
URLs produced:
/conferences/— conferences in March 20252025/ march /conferences/— paginated2025/ march/ page- 2 /conferences/— all conferences in 20252025
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: '/{year}/{month}/page-{page}'
_controller: 'Conference::list'
_arguments:
year: overwriteDemand/year
month: overwriteDemand/month
page: currentPage
- routePath: '/{year}/{month}'
_controller: 'Conference::list'
_arguments:
year: overwriteDemand/year
month: overwriteDemand/month
- routePath: '/{year}'
_controller: 'Conference::list'
_arguments:
year: overwriteDemand/year
- routePath: '/article/{conference_slug}'
_controller: 'Conference::show'
_arguments:
conference_slug: conference
defaults:
page: '1'
month: ''
year: ''
requirements:
page: '\d+'
month: '\d+'
year: '\d{4}'
aspects:
page:
type: StaticRangeMapper
start: '1'
end: '50'
month:
type: StaticValueMapper
map:
january: '01'
february: '02'
march: '03'
april: '04'
may: '05'
june: '06'
july: '07'
august: '08'
september: '09'
october: '10'
november: '11'
december: '12'
year:
type: StaticRangeMapper
start: '2000'
end: '2030'
conference_slug:
type: PersistedAliasMapper
tableName: tx_myextension_domain_model_conference
routeFieldName: slug
routeValuePrefix: '/'
Warning
Each
Static is limited to 1000 values: TYPO3 throws a
\Length when the range between
start and
end
is larger than 1000 items. This guards against brute-force requests and
cache-flooding. The ranges above stay well within that limit (31 years,
50 pages); for a wider span, narrow the range or write a custom mapper.
Beyond built-in aspects
The four built-in aspect types —
PersistedAliasMapper,
PersistedPatternMapper,
StaticValueMapper, and
StaticRangeMapper — cover the
majority of real-world cases. When they do not, TYPO3 allows extensions to
register fully custom aspect classes. A good example is
georgringer/news
, which ships its own
News,
News, and
News mappers — all thin wrappers around
Persisted
that add extension-specific defaults and fallback handling.
See also
Route Enhancements and Aspects — the Core routing reference covers custom aspect registration.
Common mistakes
- Plugin not placed on the target page
- The enhancer is configured, the URL looks right, but TYPO3 returns a 404.
Check that the plugin content element is actually present on the page
referenced in
limit.To Pages - setTargetPageUid() missing
- Links from a list plugin point back to the list page instead of the detail
page. Always set
setin PHP orTarget Page Uid () pagein Fluid when list and detail are on separate pages.Uid - Wrong route order
- A catch-all placeholder route (
/) is listed before a more specific route ({slug} /page-). The catch-all matches first and the specific route is never reached. Most-specific routes must come first — see Route order and specificity.{page} - limitToPages missing
- Without
limit, TYPO3 evaluates the enhancer for every page on the site. Performance degrades and unintended matches on unrelated pages become possible.To Pages - cHash still appearing
- A placeholder has a
requirementsregex but no aspect. OnlyStaticandRange Mapper Staticmark a parameter as static and suppressValue Mapper c. A regex requirement alone does not.Hash - Stale cache after config changes
- Site configuration is cached. After any change to
EXT:ormy_ extension/ Configuration/ Sets/ My Extension/ route- enhancers. yaml config/, clear all caches via Admin Tools > Maintenance.sites/<site- identifier>/ config. yaml