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.

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/page-2 — list, page 2
  • /conferences/typo3camp-2025 — detail view
EXT:my_extension/Configuration/Sets/MyExtension/route-enhancers.yaml
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
Copied!

Key points:

  • The pagination route /page-{page} uses a static prefix page- to distinguish it from the detail route /{conference_slug}. Without the prefix, /conferences/page-2 would match the detail route first and try to resolve page-2 as a conference slug.
  • fallbackValue: null on the PersistedAliasMapper means a deleted or hidden conference returns null to 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 — limitToPages must point to the correct page for each, and links between them need setTargetPageUid() or pageUid in Fluid.

EXT:my_extension/Configuration/Sets/MyExtension/route-enhancers.yaml
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
Copied!

In the list template, link to the detail page explicitly:

EXT:my_extension/Resources/Private/Templates/Conference/List.fluid.html
<f:link.action
    action="show"
    controller="Conference"
    arguments="{conference: conference}"
    pageUid="{settings.detailPageUid}"
>
    {conference.title}
</f:link.action>
Copied!

Store the detail page UID in TypoScript settings so it is configurable without touching PHP:

EXT:my_extension/Configuration/Sets/MyExtension/setup.typoscript
plugin.tx_myextension_conferences.settings.detailPageUid = 42
Copied!

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/typo3camp-2025 — conference detail
  • /conferences/typo3camp-2025/talks — talk list for that conference
  • /conferences/typo3camp-2025/talks/extbase-routing-demystified — talk detail
EXT:my_extension/Configuration/Sets/MyExtension/route-enhancers.yaml
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
Copied!

The {conference_slug} placeholder appears in both the Conference::show 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/2025/march — conferences in March 2025
  • /conferences/2025/march/page-2 — paginated
  • /conferences/2025 — all conferences in 2025
EXT:my_extension/Configuration/Sets/MyExtension/route-enhancers.yaml
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: '/'
Copied!

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 NewsTitle, NewsCategory, and NewsTag mappers — all thin wrappers around PersistedAliasMapper that add extension-specific defaults and fallback handling.

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 limitToPages.
setTargetPageUid() missing
Links from a list plugin point back to the list page instead of the detail page. Always set setTargetPageUid() in PHP or pageUid in Fluid when list and detail are on separate pages.
Wrong route order
A catch-all placeholder route ( /{slug}) is listed before a more specific route ( /page-{page}). The catch-all matches first and the specific route is never reached. Most-specific routes must come first — see Route order and specificity.
limitToPages missing
Without limitToPages, TYPO3 evaluates the enhancer for every page on the site. Performance degrades and unintended matches on unrelated pages become possible.
cHash still appearing
A placeholder has a requirements regex but no aspect. Only StaticRangeMapper and StaticValueMapper mark a parameter as static and suppress cHash. A regex requirement alone does not.
Stale cache after config changes
Site configuration is cached. After any change to EXT:my_extension/Configuration/Sets/MyExtension/route-enhancers.yaml or config/sites/<site-identifier>/config.yaml, clear all caches via Admin Tools > Maintenance.