Feature: #94499 - Provide additional PageTypeSource auto-create redirect source type

See forge#94499


A new source type implementation based on \TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface is added, providing the page type number as an additional value. The main use case for this source type is to provide additional source types where the source host and path are taken from a fully built URI before the page slug change occurred for a specific page type. That avoids the need for extension authors to implement a custom source type for the same task, and instead provides a custom event listener to build sources for non-zero page types. Sources can be added by implementing an event listener for \TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent.

This class features the following methods:

  • getHost(): Returns the source host for the redirect
  • getPath(): Returns the source path for the redirect
  • getPageType(): Returns the page type used to provide the host/path
  • getTargetLinkParameters(): Returns the link parameters which should be used to create the target based on t3:// syntax

Values can be set only by the constructor.


Registration of the event in your extension's Services.yaml:

    - name: event.listener
      identifier: 'my-extension/custom-page-type-redirect'
      after: 'redirects-add-page-type-zero-source'

The corresponding event listener class:

namespace MyVendor\MyExtension\Redirects;

use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Redirects\Event\SlugRedirectChangeItemCreatedEvent;
use TYPO3\CMS\Redirects\RedirectUpdate\PageTypeSource;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;

final class MyEventListener
    protected array $customPageTypes = [ 1234, 169999 ];

    public function __invoke(
        SlugRedirectChangeItemCreatedEvent $event
    ): void {
        $changeItem = $event->getSlugRedirectChangeItem();
        $sources = $changeItem->getSourcesCollection()->all();

        foreach ($this->customPageTypes as $pageType) {
            try {
                $pageTypeSource = $this->createPageTypeSource(
                if ($pageTypeSource === null) {
            } catch (UnableToLinkToPageException) {
                // Could not properly link to page. Continue to next page type

            if ($this->isDuplicate($pageTypeSource, ...$sources)) {
                // not adding duplicate,

            $sources[] = $pageTypeSource;

        // update sources
        $changeItem = $changeItem->withSourcesCollection(
            new RedirectSourceCollection(

        // update change item with updated sources

    private function isDuplicate(
        PageTypeSource $pageTypeSource,
        RedirectSourceInterface ...$sources
    ): bool {
        foreach ($sources as $existingSource) {
            $existingHost = $existingSource->getHost();
            $pageTypeSourceHost = $pageTypeSource->getHost();
            $existingPath = rtrim($existingSource->getPath(), '/');
            $pageTypeSourcePath = rtrim($pageTypeSource->getPath(), '/');
            if ($existingSource instanceof PageTypeSource
                && $existingHost === $pageTypeSourceHost
                && $existingPath === $pageTypeSourcePath
            ) {
                // we do not check for the type, as that is irrelevant. Same
                // host+path tuple would lead to duplicated redirects if
                // type differs.
                return true;
        return false;

    private function createPageTypeSource(
        int $pageUid,
        int $pageType,
        Site $site,
        SiteLanguage $siteLanguage
    ): ?PageTypeSource {
        if ($pageType === 0) {
            // pageType 0 is handled by \TYPO3\CMS\Redirects\EventListener\AddPageTypeZeroSource
            return null;

        try {
            $context = $this->getAdjustedContext();
            $uri = $site->getRouter($context)->generateUri(
                    '_language' => $siteLanguage,
                    'type' => $pageType,
            return new PageTypeSource(
                $uri->getHost() ?: '*',
                    'type' => $pageType,
        } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) {
            throw new UnableToLinkToPageException(
                    'The link to the page with ID "%d" and type "%d" could not be generated: %s',

     * Returns the adjusted current context with modified visibility settings
     * to build source url for hidden or scheduled pages.
    private function getAdjustedContext(): Context
        $adjustedVisibility = new VisibilityAspect(
        $originalContext = GeneralUtility::makeInstance(Context::class);
        $context = clone $originalContext;
        $context->setAspect('visibility', $adjustedVisibility);
        return $context;


The new PageTypeSource can be used to provide additional sources, for example, based on custom page types using full URI building, which would take configured PageTypeSuffix decorators into account. For page type 0 (default), the Core implements an event listener which adds the source based on this source class for page type 0 with AddPageTypeZeroSource event listener.