diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
index 320f276bd25f8c6ac626437f21f26ff07e4dc29d..1be0c876421e0a18879f1d77be5a8d9b60e08831 100644
--- a/.phpstorm.meta.php
+++ b/.phpstorm.meta.php
@@ -89,6 +89,7 @@ namespace PHPSTORM_META {
         'frontend.controller',
         'frontend.typoscript',
         'frontend.cache.instruction',
+        'frontend.page.information',
     );
     override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([
         'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class,
@@ -101,6 +102,7 @@ namespace PHPSTORM_META {
         'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class,
         'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class,
         'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class,
+        'frontend.page.information' => \TYPO3\CMS\Frontend\Page\PageInformation::class,
     ]));
 
     expectedArguments(
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
index cfc78ed5eb03ca8b16c14c33110561cf78185ef9..cc84c05a3104366d8b9d45a00a9d92a9f44e626e 100644
--- a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst
@@ -46,13 +46,13 @@ marked :php:`@internal`, which the core will deprecate with a compatibility laye
 
 The following public class properties have been marked "read only":
 
-* :php:`TypoScriptFrontendController->id`
-* :php:`TypoScriptFrontendController->rootLine`
-* :php:`TypoScriptFrontendController->page`
-* :php:`TypoScriptFrontendController->contentPid`
-* :php:`TypoScriptFrontendController->sys_page`
-* :php:`TypoScriptFrontendController->config` - Reading :php:`$tsfe->config['config']`
-  and :php:`$tsfe->config['rootLine']` is allowed
+* :php:`TypoScriptFrontendController->id` - Use :php:`$request->getAttribute('frontend.page.information')->getId()` instead
+* :php:`TypoScriptFrontendController->rootLine` - Use :php:`$request->getAttribute('frontend.page.information')->getRootLine()` instead
+* :php:`TypoScriptFrontendController->page` - Use :php:`$request->getAttribute('frontend.page.information')->getPageRecord()` instead
+* :php:`TypoScriptFrontendController->contentPid` - Avoid usages altogether, available as :php:`@internal` call using
+  :php:`$request->getAttribute('frontend.page.information')->getContentFromPid()`
+* :php:`TypoScriptFrontendController->sys_page` - Avoid usages altogether, create own instances when needed
+* :php:`TypoScriptFrontendController->config` - Reading :php:`$tsfe->config['config']` and :php:`$tsfe->config['rootLine']` is allowed
 * :php:`TypoScriptFrontendController->absRefPrefix`
 * :php:`TypoScriptFrontendController->cObj`
 
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst
new file mode 100644
index 0000000000000000000000000000000000000000..615286ebe602020ca4ea1a4037e8116c402f49ee
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst
@@ -0,0 +1,60 @@
+.. include:: /Includes.rst.txt
+
+.. _breaking-102715-1703254781:
+
+===================================================================
+Breaking: #102715 - Frontend "determineId()" related events changed
+===================================================================
+
+See :issue:`102715`
+
+Description
+===========
+
+With the continued refactoring of :php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController`,
+the following events have been adapted:
+
+* :php:`TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent`
+* :php:`TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent`
+* :php:`TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent`
+
+The three events no longer retrieve an instance of :php:`TypoScriptFrontendController`, the
+getter methods :php:`getController()` have been removed: The controller is instantiated
+*after* the events have been dispatched, event listeners can no longer work with this
+object.
+
+Instead, the events now contain an instance of the new DTO
+:php:`TYPO3\CMS\Frontend\Page\PageInformation`, which can be retrieved,
+manipulated by event listeners if necessary.
+
+Impact
+======
+
+Calling :php:`getController()` by consumers of above events will raise a fatal
+PHP error.
+
+Also note the events may not be dispatched anymore when the middleware
+:php:`TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization` creates
+early responses.
+
+
+Affected installations
+======================
+
+Those events are in place for a couple of special cases during early frontend rendering.
+Most instances will not be affected, but some extensions may register event listeners.
+
+
+Migration
+=========
+
+Use method :php:`getPageInformation()` instead to retrieve calculated page state at
+this point in the Frontend rendering chain. Event listeners that manipulate that
+object should set it again within the event using :php:`setPageInformation()`.
+
+In case middleware :php:`TypoScriptFrontendInitialization` no longer dispatches an event
+when it created an early response on its own, an own middleware can be added around
+that middleware to retrieve and further manipulate a response if needed.
+
+
+.. index:: Frontend, PHP-API, PartiallyScanned, ext:frontend
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst
new file mode 100644
index 0000000000000000000000000000000000000000..209d358e32bc52b7606bcb62ccd251bf587fbb11
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst
@@ -0,0 +1,43 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-102715-1703261072:
+
+==================================================================
+Feature: #102715 - New frontend.page.information Request attribute
+==================================================================
+
+See :issue:`102715`
+
+Description
+===========
+
+TYPO3 v13 introduces the new Frontend related Request attribute :php:`frontend.page.information`
+implemented by class :php:`TYPO3\CMS\Frontend\Page\PageInformation`. The object aims to replace
+various page related properties of :php:`TYPO3\CMS\Frontend\Controller\TyposcriptFrontendController`.
+
+Note the class is currently still marked as experimental. Extension authors are however encouraged
+to use information from this Request attribute instead of the :php:`TyposcriptFrontendController`
+properties already: TYPO3 core v13 will try to not break especially the getters / properties not
+marked as :php:`@internal`.
+
+
+Impact
+======
+
+There are three properties in :php:`TyposcriptFrontendController` frequently used by extensions,
+which are now modeled in :php:`TYPO3\CMS\Frontend\Page\PageInformation`. The attribute is
+attached to the PSR-7 frontend Request by middleware :php:`TypoScriptFrontendInitialization`,
+middlewares below can rely on existence of that attribute. Examples:
+
+.. code-block:: php
+
+    $pageInformation = $request->getAttribute('frontend.page.information');
+    // Formerly $tsfe->id
+    $id = $pageInformation->getId();
+    // Formerly $tsfe->page
+    $page = $pageInformation->getPageRecord();
+    // Formerly $tsfe->rootLine
+    $rootLine = $pageInformation->getRootLine();
+
+
+.. index:: Frontend, PHP-API, ext:frontend
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index bbbd9aa6b9c1cef4b43d66cdaa42e23ce0067832..10c0ef276bc8cb5cb4ec5a9e8b11734c2c5e5576 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -26,20 +26,11 @@ use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
-use TYPO3\CMS\Core\Context\LanguageAspect;
-use TYPO3\CMS\Core\Context\LanguageAspectFactory;
 use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
 use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException;
-use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
-use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
-use TYPO3\CMS\Core\Exception\Page\RootLineException;
-use TYPO3\CMS\Core\Exception\SiteNotFoundException;
-use TYPO3\CMS\Core\Http\ImmediateResponseException;
-use TYPO3\CMS\Core\Http\NormalizedParams;
 use TYPO3\CMS\Core\Http\PropagateResponseException;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
@@ -52,10 +43,7 @@ use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
-use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
-use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
-use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Type\DocType;
 use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
 use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
@@ -71,16 +59,11 @@ use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConst
 use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
-use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;
 use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent;
-use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent;
-use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent;
-use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent;
 use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent;
 use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
@@ -155,34 +138,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     public int $contentPid = 0;
 
-    /**
-     * Gets set when we are processing a page of type mountpoint with enabled overlay in getPageAndRootline()
-     * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user
-     * should be redirected to.
-     */
-    protected ?array $originalMountPointPage = null;
-
-    /**
-     * Gets set when we are processing a page of type shortcut in the early stages
-     * of the request, used later in the request to resolve the shortcut and redirect again.
-     */
-    protected ?array $originalShortcutPage = null;
-
     /**
      * Read-only! Extensions may read but never write this property!
      */
     public ?PageRepository $sys_page = null;
 
-    /**
-     * Is set to > 0 if the page could not be resolved. This will then result in early returns when resolving the page.
-     */
-    protected int $pageNotFound = 0;
-
-    /**
-     * Array containing a history of why a requested page was not accessible.
-     */
-    protected array $pageAccessFailureHistory = [];
-
     /**
      * @internal
      */
@@ -388,7 +348,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->context = $context;
         $this->site = $site;
         $this->language = $siteLanguage;
-        $this->setPageArguments($pageArguments);
+        $this->pageArguments = $pageArguments;
+        $this->id = $pageArguments->getPageId();
         $this->uniqueString = md5(microtime());
         $this->initPageRenderer();
         $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
@@ -420,482 +381,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->contentType = $contentType;
     }
 
-    /**
-     * Resolves the page id and sets up several related properties.
-     *
-     * At this point, the Context object already contains relevant preview
-     * settings (if a backend user is logged in etc).
-     *
-     * If $this->id is not set at all, the method does its best to set the
-     * value to an integer. Resolving is based on this options:
-     *
-     * - Finding the domain record start page
-     * - First visible page
-     * - Relocating the id below the site if outside the site / domain
-     *
-     * The following properties may be set up or updated:
-     *
-     * - id
-     * - sys_page
-     * - sys_page->where_groupAccess
-     * - sys_page->where_hid_del
-     * - register['SYS_LASTCHANGED']
-     * - pageNotFound
-     *
-     * Via getPageAndRootline()
-     *
-     * - rootLine
-     * - page
-     * - MP
-     * - originalShortcutPage
-     * - originalMountPointPage
-     * - pageAccessFailureHistory['direct_access']
-     * - pageNotFound
-     *
-     * @internal
-     */
-    public function determineId(ServerRequestInterface $request): ?ResponseInterface
-    {
-        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
-
-        $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
-        $eventDispatcher->dispatch(new BeforePageIsResolvedEvent($this, $request));
-
-        $timeTracker = $this->getTimeTracker();
-        $timeTracker->push('determineId rootLine/');
-        try {
-            // Sets ->page and ->rootline information based on ->id. ->id may change during this operation.
-            // If the found Page ID is not within the site, then pageNotFound is set.
-            $this->getPageAndRootline($request);
-            // Checks if the rootPageId of the site is in the resolved rootLine.
-            // This is necessary so that references to page-id's via ?id=123 from other sites are not possible.
-            $siteRootWithinRootlineFound = false;
-            foreach ($this->rootLine as $pageInRootLine) {
-                if ((int)$pageInRootLine['uid'] === $this->site->getRootPageId()) {
-                    $siteRootWithinRootlineFound = true;
-                    break;
-                }
-            }
-            // Page is 'not found' in case the id was outside the domain, code 3
-            // This can only happen if there was a shortcut. So $this->page is now the shortcut target
-            // But the original page is in $this->originalShortcutPage.
-            // This only happens if people actually call TYPO3 with index.php?id=123 where 123 is in a different
-            // page tree. This is not allowed.
-            $directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0);
-            if (!$siteRootWithinRootlineFound && $directlyRequestedId && (int)($this->originalShortcutPage['uid'] ?? 0) !== $directlyRequestedId) {
-                $this->pageNotFound = 3;
-                $this->id = $this->site->getRootPageId();
-                // re-get the page and rootline if the id was not found.
-                $this->getPageAndRootline($request);
-            }
-        } catch (ShortcutTargetPageNotFoundException) {
-            $this->pageNotFound = 1;
-        }
-        $timeTracker->pull();
-
-        $event = new AfterPageWithRootLineIsResolvedEvent($this, $request);
-        $event = $eventDispatcher->dispatch($event);
-        if ($event->getResponse()) {
-            return $event->getResponse();
-        }
-
-        $response = null;
-        try {
-            $this->evaluatePageNotFound($this->pageNotFound, $request);
-
-            // Setting language and fetch translated page
-            $this->settingLanguage($request);
-            // Check the "content_from_pid" field of the resolved page
-            $this->contentPid = $this->resolveContentPid($request);
-
-            // Update SYS_LASTCHANGED at the very last, when $this->page might be changed
-            // by settingLanguage() and the $this->page was finally resolved
-            $this->setRegisterValueForSysLastChanged($this->page);
-        } catch (PropagateResponseException $e) {
-            $response = $e->getResponse();
-        }
-
-        $event = new AfterPageAndLanguageIsResolvedEvent($this, $request, $response);
-        $eventDispatcher->dispatch($event);
-        return $event->getResponse();
-    }
-
-    /**
-     * If $this->pageNotFound is set, then throw an exception to stop further page generation process
-     */
-    protected function evaluatePageNotFound(int $pageNotFoundNumber, ServerRequestInterface $request): void
-    {
-        if (!$pageNotFoundNumber) {
-            return;
-        }
-        $response = match ($pageNotFoundNumber) {
-            1 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
-                $request,
-                'ID was not an accessible page',
-                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
-            ),
-            2 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
-                $request,
-                'Subsection was found and not accessible',
-                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
-            ),
-            3 => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                $request,
-                'ID was outside the domain',
-                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
-            ),
-            default => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                $request,
-                'Unspecified error',
-                $this->getPageAccessFailureReasons()
-            ),
-        };
-        throw new PropagateResponseException($response, 1533931329);
-    }
-
-    /**
-     * Loads the page and root line records based on $this->id
-     *
-     * A final page and the matching root line are determined and loaded by
-     * the algorithm defined by this method.
-     *
-     * First it loads the initial page from the page repository for $this->id.
-     * If that can't be loaded directly, it gets the root line for $this->id.
-     * It walks up the root line towards the root page until the page
-     * repository can deliver a page record. (The loading restrictions of
-     * the root line records are more liberal than that of the page record.)
-     *
-     * Now the page type is evaluated and handled if necessary. If the page is
-     * a short cut, it is replaced by the target page. If the page is a mount
-     * point in overlay mode, the page is replaced by the mounted page.
-     *
-     * After this potential replacements are done, the root line is loaded
-     * (again) for this page record. It walks up the root line up to
-     * the first viewable record.
-     *
-     * (While upon the first accessibility check of the root line it was done
-     * by loading page by page from the page repository, this time the method
-     * checkRootlineForIncludeSection() is used to find the most distant
-     * accessible page within the root line.)
-     *
-     * Having found the final page id, the page record and the root line are
-     * loaded for last time by this method.
-     *
-     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
-     * or root lines.
-     *
-     * May set or update these properties:
-     *
-     * @see TypoScriptFrontendController::$id
-     * @see TypoScriptFrontendController::$MP
-     * @see TypoScriptFrontendController::$page
-     * @see TypoScriptFrontendController::$pageNotFound
-     * @see TypoScriptFrontendController::$pageAccessFailureHistory
-     * @see TypoScriptFrontendController::$originalMountPointPage
-     * @see TypoScriptFrontendController::$originalShortcutPage
-     *
-     * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException
-     * @throws PageNotFoundException
-     * @throws ShortcutTargetPageNotFoundException
-     */
-    protected function getPageAndRootline(ServerRequestInterface $request): void
-    {
-        $requestedPageRowWithoutGroupCheck = [];
-        $this->page = $this->sys_page->getPage($this->id);
-        if (empty($this->page)) {
-            // If no page, we try to find the page above in the rootLine.
-            // Page is 'not found' in case the id itself was not an accessible page. code 1
-            $this->pageNotFound = 1;
-            $requestedPageIsHidden = false;
-            try {
-                $hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? '';
-                $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
-                if (!empty($hiddenField) && !$includeHiddenPages) {
-                    // Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays)
-                    $rawPageRecord = $this->sys_page->getPage_noCheck($this->id);
-
-                    // If page record could not be resolved throw exception
-                    if ($rawPageRecord === []) {
-                        $message = 'The requested page does not exist!';
-                        try {
-                            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                                $request,
-                                $message,
-                                $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
-                            );
-                            throw new PropagateResponseException($response, 1674144383);
-                        } catch (PageNotFoundException $e) {
-                            throw new PageNotFoundException($message, 1674539331);
-                        }
-                    }
-
-                    $requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField];
-                }
-
-                $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
-                if (!empty($requestedPageRowWithoutGroupCheck)) {
-                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
-                }
-                $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
-                if (!empty($this->rootLine)) {
-                    $c = count($this->rootLine) - 1;
-                    while ($c > 0) {
-                        // Add to page access failure history:
-                        $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
-                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
-                        $c--;
-                        $this->id = (int)$this->rootLine[$c]['uid'];
-                        $this->page = $this->sys_page->getPage($this->id);
-                        if (!empty($this->page)) {
-                            break;
-                        }
-                    }
-                }
-            } catch (RootLineException) {
-                $this->rootLine = [];
-            }
-            // If still no page...
-            if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($this->page))) {
-                $message = 'The requested page does not exist!';
-                try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                        $request,
-                        $message,
-                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
-                    );
-                    throw new PropagateResponseException($response, 1533931330);
-                } catch (PageNotFoundException $e) {
-                    throw new PageNotFoundException($message, 1301648780);
-                }
-            }
-        }
-        // Spacer and sysfolders is not accessible in frontend
-        $pageDoktype = (int)($this->page['doktype'] ?? 0);
-        $isSpacerOrSysfolder = $pageDoktype === PageRepository::DOKTYPE_SPACER || $pageDoktype === PageRepository::DOKTYPE_SYSFOLDER;
-        // Page itself is not accessible, but the parent page is a spacer/sysfolder
-        if ($isSpacerOrSysfolder && !empty($requestedPageRowWithoutGroupCheck)) {
-            try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
-                    $request,
-                    'Subsection was found and not accessible',
-                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
-                );
-                throw new PropagateResponseException($response, 1633171038);
-            } catch (PageNotFoundException $e) {
-                throw new PageNotFoundException('Subsection was found and not accessible', 1633171172);
-            }
-        }
-
-        if ($isSpacerOrSysfolder) {
-            $message = 'The requested page does not exist!';
-            try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                    $request,
-                    $message,
-                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
-                );
-                throw new PropagateResponseException($response, 1533931343);
-            } catch (PageNotFoundException $e) {
-                throw new PageNotFoundException($message, 1301648781);
-            }
-        }
-        // Is the ID a link to another page??
-        if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
-            // We need to clear MP if the page is a shortcut. Reason is if the shortcut goes to another page, then we LEAVE the rootline which the MP expects.
-            $this->MP = '';
-            // saving the page so that we can check later - when we know
-            // about languages - whether we took the correct shortcut or
-            // whether a translation of the page overwrites the shortcut
-            // target and we need to follow the new target
-            $this->settingLanguage($request);
-            $this->originalShortcutPage = $this->page;
-            $this->page = $this->sys_page->resolveShortcutPage($this->page, true);
-            $this->id = (int)$this->page['uid'];
-            $pageDoktype = (int)($this->page['doktype'] ?? 0);
-        }
-        // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
-        // it must never be accessible directly, but only in the mountpoint context. Therefore we change
-        // the current ID and the user is redirected by checkPageForMountpointRedirect().
-        if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
-            $this->originalMountPointPage = $this->page;
-            $this->page = $this->sys_page->getPage($this->page['mount_pid']);
-            if (empty($this->page)) {
-                $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
-                    . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
-                throw new PageNotFoundException($message, 1402043263);
-            }
-            // If the current page is a shortcut, the MP parameter will be replaced
-            if ($this->MP === '' || !empty($this->originalShortcutPage)) {
-                $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
-            } else {
-                $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
-            }
-            $this->id = (int)$this->page['uid'];
-        }
-        // Gets the rootLine
-        try {
-            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
-        } catch (RootLineException $e) {
-            $this->rootLine = [];
-        }
-        // If not rootline we're off...
-        if (empty($this->rootLine)) {
-            $message = 'The requested page didn\'t have a proper connection to the tree-root!';
-            $this->logPageAccessFailure($message, $request);
-            try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
-                    $request,
-                    $message,
-                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
-                );
-                throw new PropagateResponseException($response, 1533931350);
-            } catch (AbstractServerErrorException $e) {
-                $this->logger->error($message, ['exception' => $e]);
-                $exceptionClass = get_class($e);
-                throw new $exceptionClass($message, 1301648167);
-            }
-        }
-        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
-        if ($this->checkRootlineForIncludeSection()) {
-            if (empty($this->rootLine)) {
-                $message = 'The requested page does not exist!';
-                try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                        $request,
-                        $message,
-                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
-                    );
-                    throw new PropagateResponseException($response, 1533931351);
-                } catch (AbstractServerErrorException $e) {
-                    $this->logger->warning($message);
-                    $exceptionClass = get_class($e);
-                    throw new $exceptionClass($message, 1301648234);
-                }
-            } else {
-                $el = reset($this->rootLine);
-                $this->id = (int)$el['uid'];
-                $this->page = $this->sys_page->getPage($this->id);
-                try {
-                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
-                } catch (RootLineException $e) {
-                    $this->rootLine = [];
-                }
-            }
-        }
-    }
-
-    /**
-     * Checks if visibility of the page is blocked upwards in the root line.
-     *
-     * If any page in the root line is blocking visibility, true is returned.
-     *
-     * All pages from the blocking page downwards are removed from the root
-     * line, so that the remaining pages can be used to relocate the page up
-     * to lowest visible page.
-     *
-     * The blocking feature of a page must be turned on by setting the page
-     * record field 'extendToSubpages' to 1 in case of hidden, starttime,
-     * endtime or fe_group restrictions.
-     *
-     * Additionally, this method checks for backend user sections in root line
-     * and if found, evaluates if a backend user is logged in and has access.
-     *
-     * Recyclers are also checked and trigger page not found if found in root
-     * line.
-     *
-     * @todo Find a better name, i.e. checkVisibilityByRootLine
-     * @todo Invert boolean return value. Return true if visible.
-     */
-    protected function checkRootlineForIncludeSection(): bool
-    {
-        $c = count($this->rootLine);
-        $removeTheRestFlag = false;
-        $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
-        for ($a = 0; $a < $c; $a++) {
-            if (!$accessVoter->accessGrantedForPageInRootLine($this->rootLine[$a], $this->context)) {
-                // Add to page access failure history and mark the page as not found
-                // Keep the rootline however to trigger an access denied error instead of a service unavailable error
-                $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a];
-                $this->pageNotFound = 2;
-            }
-
-            if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
-                // If there is a backend user logged in, check if they have read access to the page:
-                if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
-                    // If there was no page selected, the user apparently did not have read access to the
-                    // current page (not position in rootline) and we set the remove-flag...
-                    if (!$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
-                        $removeTheRestFlag = true;
-                    }
-                } else {
-                    // Don't go here, if there is no backend user logged in.
-                    $removeTheRestFlag = true;
-                }
-            }
-            if ($removeTheRestFlag) {
-                // Page is 'not found' in case a subsection was found and not accessible, code 2
-                $this->pageNotFound = 2;
-                unset($this->rootLine[$a]);
-            }
-        }
-        return $removeTheRestFlag;
-    }
-
-    /**
-     * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions.
-     * That data can be used inside a page-not-found handler
-     *
-     * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
-     * @return array Summary of why page access was not allowed.
-     * @internal
-     */
-    public function getPageAccessFailureReasons(string $failureReasonCode = null): array
-    {
-        $output = [];
-        if ($failureReasonCode) {
-            $output['code'] = $failureReasonCode;
-        }
-        $combinedRecords = array_merge(
-            is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]],
-            is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : []
-        );
-        if (!empty($combinedRecords)) {
-            $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
-            foreach ($combinedRecords as $k => $pagerec) {
-                // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course
-                // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the
-                // extendToSubpages flag set, hence checked only then!
-                if (!$k || $pagerec['extendToSubpages']) {
-                    if ($pagerec['hidden'] ?? false) {
-                        $output['hidden'][$pagerec['uid']] = true;
-                    }
-                    if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
-                        $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
-                    }
-                    if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
-                        $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
-                    }
-                    if (!$accessVoter->groupAccessGranted('pages', $pagerec, $this->context)) {
-                        $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
-                    }
-                }
-            }
-        }
-        return $output;
-    }
-
-    protected function setPageArguments(PageArguments $pageArguments): void
-    {
-        $this->pageArguments = $pageArguments;
-        $this->id = $pageArguments->getPageId();
-        // We store the originally requested id
-        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
-            $this->MP = (string)($pageArguments->getArguments()['MP'] ?? '');
-            // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
-            $this->MP = preg_replace('/[^0-9,-]/', '', $this->MP);
-        }
-    }
-
     /**
      * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
      * Excluded parameters are not taken into account when calculating the hash base.
@@ -1409,121 +894,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         return $this->id . '_' . sha1(serialize($hashParameters));
     }
 
-    /**
-     * Setting the language key that will be used by the current page.
-     * In this function it should be checked, 1) that this language exists, 2) that a page_overlay_record exists, .. and if not the default language, 0 (zero), should be set.
-     */
-    protected function settingLanguage(ServerRequestInterface $request): void
-    {
-        // Get values from site language
-        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
-
-        $languageId = $languageAspect->getId();
-        $languageContentId = $languageAspect->getContentId();
-
-        $pageTranslationVisibility = new PageTranslationVisibility((int)($this->page['l18n_cfg'] ?? 0));
-        // If the incoming language is set to another language than default
-        if ($languageAspect->getId() > 0) {
-            // Request the translation for the requested language
-            $olRec = $this->sys_page->getPageOverlay($this->page, $languageAspect);
-            $overlaidLanguageId = (int)($olRec['sys_language_uid'] ?? 0);
-            if ($overlaidLanguageId !== $languageAspect->getId()) {
-                // If requested translation is not available
-                if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                        $request,
-                        'Page is not available in the requested language.',
-                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
-                    );
-                    throw new PropagateResponseException($response, 1533931388);
-                }
-                switch ($languageAspect->getLegacyLanguageMode()) {
-                    case 'strict':
-                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                            $request,
-                            'Page is not available in the requested language (strict).',
-                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
-                        );
-                        throw new PropagateResponseException($response, 1533931395);
-                    case 'content_fallback':
-                        // Setting content uid (but leaving the sys_language_uid) when a content_fallback
-                        // value was found.
-                        foreach ($languageAspect->getFallbackChain() as $orderValue) {
-                            if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
-                                $languageContentId = 0;
-                                break;
-                            }
-                            if (MathUtility::canBeInterpretedAsInteger($orderValue) && $overlaidLanguageId === (int)$orderValue) {
-                                $languageContentId = (int)$orderValue;
-                                break;
-                            }
-                            if ($orderValue === 'pageNotFound') {
-                                // The existing fallbacks have not been found, but instead of continuing
-                                // page rendering with default language, a "page not found" message should be shown
-                                // instead.
-                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                                    $request,
-                                    'Page is not available in the requested language (fallbacks did not apply).',
-                                    ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
-                                );
-                                throw new PropagateResponseException($response, 1533931402);
-                            }
-                        }
-                        break;
-                    default:
-                        // Default is that everything defaults to the default language...
-                        $languageId = ($languageContentId = 0);
-                }
-            }
-
-            // Define the language aspect again now
-            $languageAspect = GeneralUtility::makeInstance(
-                LanguageAspect::class,
-                $languageId,
-                $languageContentId,
-                $languageAspect->getOverlayType(),
-                $languageAspect->getFallbackChain()
-            );
-
-            // Setting the $this->page if an overlay record was found (which it is only if a language is used)
-            // Doing this ensures that page properties like the page title are resolved in the correct language
-            $this->page = $olRec;
-        }
-
-        // Set the language aspect
-        $this->context->setAspect('language', $languageAspect);
-
-        // Setting sys_language_uid inside sys-page by creating a new page repository
-        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
-        // If default language is not available
-        if ((!$languageAspect->getContentId() || !$languageAspect->getId())
-            && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
-        ) {
-            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                $request,
-                'Page is not available in default language.',
-                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
-            );
-            throw new PropagateResponseException($response, 1533931423);
-        }
-
-        if ($languageAspect->getId() > 0) {
-            $this->updateRootLinesWithTranslations();
-        }
-    }
-
-    /**
-     * Updating content of the two rootLines IF the language key is set!
-     */
-    protected function updateRootLinesWithTranslations(): void
-    {
-        try {
-            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
-        } catch (RootLineException $e) {
-            $this->rootLine = [];
-        }
-    }
-
     /**
      * Calculates and sets the internal linkVars based upon the current request parameters
      * and the setting "config.linkVars".
@@ -1542,63 +912,19 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
-     * Returns URI of target page, if the current page is an overlaid mountpoint.
-     *
-     * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
-     * and is accessed directly, the user will be redirected to the mountpoint context.
-     * @internal
-     */
-    public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
-    {
-        if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
-            return $this->getUriToCurrentPageForRedirect($request);
-        }
-
-        return null;
-    }
-
-    /**
-     * Returns URI of target page, if the current page is a Shortcut.
-     *
-     * If the current page is of type shortcut and accessed directly via its URL,
-     * the user will be redirected to shortcut target.
+     * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
      *
      * @internal
      */
-    public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
-    {
-        if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
-            // Check if the shortcut page is actually on the current site, if not, this is a "page not found"
-            // because the request was www.mydomain.com/?id=23 where page ID 23 (which is a shortcut) is on another domain/site.
-            if ((int)($request->getQueryParams()['id'] ?? 0) > 0) {
-                try {
-                    $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($this->originalShortcutPage['l10n_parent'] ?: $this->originalShortcutPage['uid']);
-                } catch (SiteNotFoundException $e) {
-                    $site = null;
-                }
-                if ($site !== $this->site) {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                        $request,
-                        'ID was outside the domain',
-                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
-                    );
-                    throw new ImmediateResponseException($response, 1638022483);
-                }
-            }
-            return $this->getUriToCurrentPageForRedirect($request);
-        }
-
-        return null;
-    }
-
-    /**
-     * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
-     */
-    protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
+    public function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
     {
         $this->calculateLinkVars($request->getQueryParams());
-        $parameter = $this->page['uid'];
-        $type = (int)($this->pageArguments->getPageType() ?: 0);
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $pageRecord = $pageInformation->getPageRecord();
+        $parameter = $pageRecord['uid'];
+        /** @var PageArguments $pageArguments */
+        $pageArguments = $request->getAttribute('routing');
+        $type = (int)($pageArguments->getPageType() ?: 0);
         if ($type) {
             $parameter .= ',' . $type;
         }
@@ -1688,20 +1014,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
     }
 
-    /**
-     * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use,
-     * so the register reflects the state of the translated page, not the page in the default language.
-     *
-     * @see setSysLastChanged()
-     */
-    protected function setRegisterValueForSysLastChanged(array $page): void
-    {
-        $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp'];
-        if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) {
-            $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED'];
-        }
-    }
-
     /**
      * Adds tags to this page's cache entry, you can then f.e. remove cache
      * entries by tag
@@ -1716,30 +1028,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         return $this->pageCacheTags;
     }
 
-    /**
-     * Check the value of "content_from_pid" of the current page record, and see if the current request
-     * should actually show content from another page.
-     *
-     * By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages)
-     * are evaluated as well.
-     *
-     * @param ServerRequestInterface $request
-     * @return int the current page ID or another one if resolved properly - usually set to $this->contentPid
-     */
-    protected function resolveContentPid(ServerRequestInterface $request): int
-    {
-        if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) {
-            return $this->id;
-        }
-        // make REAL copy of TSFE object - not reference!
-        $temp_copy_TSFE = clone $this;
-        // Set ->id to the content_from_pid value - we are going to evaluate this pid as was it a given id for a page-display!
-        $temp_copy_TSFE->id = (int)$this->page['content_from_pid'];
-        $temp_copy_TSFE->MP = '';
-        $temp_copy_TSFE->getPageAndRootline($request);
-        return $temp_copy_TSFE->id;
-    }
-
     /**
      * Sets up TypoScript "config." options and set properties in $TSFE.
      *
@@ -2354,18 +1642,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         return $additionalHeaders;
     }
 
-    /**
-     * Log the page access failure with additional request information
-     */
-    protected function logPageAccessFailure(string $message, ServerRequestInterface $request): void
-    {
-        $context = ['pageId' => $this->id];
-        if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) {
-            $context['requestUrl'] = $normalizedParams->getRequestUrl();
-        }
-        $this->logger->error($message, $context);
-    }
-
     protected function getBackendUser(): ?FrontendBackendUserAuthentication
     {
         return $GLOBALS['BE_USER'] ?? null;
diff --git a/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php
index 4b48bb928f435e4ac79e3dbfb3c42e176401fc97..8019199f0256e9ec1b8fc02b4facf65e389bc328 100644
--- a/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php
@@ -19,7 +19,7 @@ namespace TYPO3\CMS\Frontend\Event;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageInformation;
 
 /**
  * A PSR-14 event fired in the frontend process after a given page has been resolved including
@@ -31,19 +31,24 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 final class AfterPageAndLanguageIsResolvedEvent
 {
     public function __construct(
-        private TypoScriptFrontendController $controller,
-        private ServerRequestInterface $request,
+        private readonly ServerRequestInterface $request,
+        private PageInformation $pageInformation,
         private ?ResponseInterface $response
     ) {}
 
-    public function getController(): TypoScriptFrontendController
+    public function getRequest(): ServerRequestInterface
     {
-        return $this->controller;
+        return $this->request;
     }
 
-    public function getRequest(): ServerRequestInterface
+    public function getPageInformation(): PageInformation
     {
-        return $this->request;
+        return $this->pageInformation;
+    }
+
+    public function setPageInformation(PageInformation $pageInformation): void
+    {
+        $this->pageInformation = $pageInformation;
     }
 
     public function getResponse(): ?ResponseInterface
diff --git a/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php
index 9cfbae08d909ccc29347644d8be01cee92b1e370..69e4e9d31c400608ab5f23c63368d37edfb63418 100644
--- a/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php
@@ -19,7 +19,7 @@ namespace TYPO3\CMS\Frontend\Event;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageInformation;
 
 /**
  * A PSR-14 event fired in the frontend process after a given page has been resolved with permissions, rootline etc.
@@ -32,15 +32,10 @@ final class AfterPageWithRootLineIsResolvedEvent
     private ?ResponseInterface $response = null;
 
     public function __construct(
-        private TypoScriptFrontendController $controller,
-        private ServerRequestInterface $request
+        private readonly ServerRequestInterface $request,
+        private PageInformation $pageInformation,
     ) {}
 
-    public function getController(): TypoScriptFrontendController
-    {
-        return $this->controller;
-    }
-
     public function getRequest(): ServerRequestInterface
     {
         return $this->request;
@@ -55,4 +50,14 @@ final class AfterPageWithRootLineIsResolvedEvent
     {
         return $this->response;
     }
+
+    public function getPageInformation(): PageInformation
+    {
+        return $this->pageInformation;
+    }
+
+    public function setPageInformation(PageInformation $pageInformation): void
+    {
+        $this->pageInformation = $pageInformation;
+    }
 }
diff --git a/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php
index e93e7e4dac5606721d587f54bc3ca1fffc2f1b22..b216d436e56fffbecc8d918a703749891ed025df 100644
--- a/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php
@@ -18,7 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Frontend\Event;
 
 use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageInformation;
 
 /**
  * A PSR-14 event fired before the frontend process is trying to fully resolve a given page
@@ -30,17 +30,22 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 final class BeforePageIsResolvedEvent
 {
     public function __construct(
-        private TypoScriptFrontendController $controller,
-        private ServerRequestInterface $request
+        private readonly ServerRequestInterface $request,
+        private PageInformation $pageInformation,
     ) {}
 
-    public function getController(): TypoScriptFrontendController
+    public function getRequest(): ServerRequestInterface
     {
-        return $this->controller;
+        return $this->request;
     }
 
-    public function getRequest(): ServerRequestInterface
+    public function getPageInformation(): PageInformation
     {
-        return $this->request;
+        return $this->pageInformation;
+    }
+
+    public function setPageInformation(PageInformation $pageInformation): void
+    {
+        $this->pageInformation = $pageInformation;
     }
 }
diff --git a/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php b/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php
index 8d6bc78729ada1d4f902c442967c34d3f04f1deb..24b7a139def2bb46902d7f346a51fdd1245b1228 100644
--- a/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php
+++ b/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php
@@ -24,9 +24,11 @@ use Psr\Http\Server\RequestHandlerInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\ImmediateResponseException;
 use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Routing\PageArguments;
+use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
@@ -86,7 +88,7 @@ class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareI
             return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
                 $request,
                 'Page of type "External URL" could not be resolved properly',
-                $controller->getPageAccessFailureReasons(PageAccessFailureReasons::INVALID_EXTERNAL_URL)
+                ['code' => PageAccessFailureReasons::INVALID_EXTERNAL_URL]
             );
         }
 
@@ -95,12 +97,68 @@ class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareI
 
     protected function getRedirectUri(ServerRequestInterface $request): ?string
     {
-        $controller = $request->getAttribute('frontend.controller');
-        $redirectToUri = $controller->getRedirectUriForShortcut($request);
+        $redirectToUri = $this->getRedirectUriForShortcut($request);
         if ($redirectToUri !== null) {
             return $redirectToUri;
         }
-        return $controller->getRedirectUriForMountPoint($request);
+        return $this->getRedirectUriForMountPoint($request);
+    }
+
+    /**
+     * Returns URI of target page, if the current page is a Shortcut.
+     *
+     * If the current page is of type shortcut and accessed directly via its URL,
+     * the user will be redirected to shortcut target.
+     */
+    protected function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
+    {
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $originalShortcutPageRecord = $pageInformation->getOriginalShortcutPageRecord();
+        if (!empty($originalShortcutPageRecord)
+            && $originalShortcutPageRecord['doktype'] == PageRepository::DOKTYPE_SHORTCUT
+        ) {
+            // Check if the shortcut page is actually on the current site, if not, this is a "page not found"
+            // because the request was www.mydomain.com/?id=23 where page ID 23 (which is a shortcut) is on another domain/site.
+            if ((int)($request->getQueryParams()['id'] ?? 0) > 0) {
+                try {
+                    $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
+                    $targetSite = $siteFinder->getSiteByPageId($originalShortcutPageRecord['l10n_parent'] ?: $originalShortcutPageRecord['uid']);
+                } catch (SiteNotFoundException) {
+                    $targetSite = null;
+                }
+                $site = $request->getAttribute('site');
+                if ($targetSite !== $site) {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'ID was outside the domain',
+                        ['code' => PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH]
+                    );
+                    throw new ImmediateResponseException($response, 1638022483);
+                }
+            }
+            $controller = $request->getAttribute('frontend.controller');
+            return $controller->getUriToCurrentPageForRedirect($request);
+        }
+        return null;
+    }
+
+    /**
+     * Returns URI of target page, if the current page is an overlaid mountpoint.
+     *
+     * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
+     * and is accessed directly, the user will be redirected to the mountpoint context.
+     */
+    protected function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
+    {
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $originalMountPointPageRecord = $pageInformation->getOriginalMountPointPageRecord();
+        if (!empty($originalMountPointPageRecord)
+            && (int)$originalMountPointPageRecord['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT
+        ) {
+            $controller = $request->getAttribute('frontend.controller');
+            return $controller->getUriToCurrentPageForRedirect($request);
+        }
+        return null;
     }
 
     /**
diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
index 98ed0694204b1ca7ea87846484fef671cf55dfe7..7b6660327638a9daf85ad64e0ec53b260b4897f7 100644
--- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
+++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
@@ -17,20 +17,41 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Frontend\Middleware;
 
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerAwareTrait;
+use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\LanguageAspect;
+use TYPO3\CMS\Core\Context\LanguageAspectFactory;
+use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
+use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException;
+use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
+use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
+use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
+use TYPO3\CMS\Core\Exception\Page\RootLineException;
+use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Http\PropagateResponseException;
 use TYPO3\CMS\Core\Routing\PageArguments;
-use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\TimeTracker\TimeTracker;
+use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
 use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent;
+use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent;
+use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
+use TYPO3\CMS\Frontend\Page\PageInformation;
 
 /**
  * Creates an instance of TypoScriptFrontendController and makes this globally available
@@ -43,8 +64,26 @@ use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
  */
 final class TypoScriptFrontendInitialization implements MiddlewareInterface
 {
+    use LoggerAwareTrait;
+
+    /**
+     * Is set to > 0 if the page could not be resolved. This will then result in early returns when resolving the page.
+     *
+     * @todo: Property needs to fall and class should no longer be marked "shared: false"
+     */
+    private int $pageNotFound = 0;
+
+    /**
+     * Array containing a history of why a requested page was not accessible.
+     *
+     * @todo: Property needs to fall and class should no longer be marked "shared: false"
+     */
+    private array $pageAccessFailureHistory = [];
+
     public function __construct(
-        private readonly Context $context
+        private readonly Context $context,
+        private readonly EventDispatcherInterface $eventDispatcher,
+        private readonly TimeTracker $timeTracker,
     ) {}
 
     /**
@@ -67,20 +106,35 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
             $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.');
         }
 
-        $GLOBALS['TYPO3_REQUEST'] = $request;
-        /** @var Site $site */
         $site = $request->getAttribute('site');
         $pageArguments = $request->getAttribute('routing');
         if (!$pageArguments instanceof PageArguments) {
-            // Page Arguments must be set in order to validate. This middleware only works if PageArguments
-            // is available, and is usually combined with the Page Resolver middleware
-            return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
-                $request,
-                'Page Arguments could not be resolved',
-                ['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS]
+            throw new \RuntimeException(
+                'PageArguments request attribute "routing" not found or valid. A previous middleware should have prepared this.',
+                1703150865
             );
         }
 
+        // @todo: It would be better to move the initial creation of PageInformation into 'determineId()'
+        //        and let the method return that object. This would make code flow more clean and allows
+        //        extraction of 'determineId()' to a (stateless) service class.
+        $pageInformation = new PageInformation();
+        $pageInformation->setId($pageArguments->getPageId());
+        $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class));
+        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids'] ?? false) {
+            $mountPoint = (string)($pageArguments->getArguments()['MP'] ?? '');
+            // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
+            $mountPoint = preg_replace('/[^0-9,-]/', '', $mountPoint);
+            $pageInformation->setMountPoint($mountPoint);
+        }
+
+        $directResponse = $this->determineId($request, $pageInformation);
+        if ($directResponse) {
+            return $directResponse;
+        }
+        $request = $request->withAttribute('frontend.page.information', $pageInformation);
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+
         $controller = GeneralUtility::makeInstance(
             TypoScriptFrontendController::class,
             $this->context,
@@ -88,10 +142,24 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
             $request->getAttribute('language', $site->getDefaultLanguage()),
             $pageArguments
         );
-        $directResponse = $controller->determineId($request);
-        if ($directResponse) {
-            return $directResponse;
+        // b/w compat layer
+        $controller->id = $pageInformation->getId();
+        $controller->sys_page = $pageInformation->getPageRepository();
+        $controller->page = $pageInformation->getPageRecord();
+        $controller->MP = $pageInformation->getMountPoint();
+        $controller->contentPid = $pageInformation->getContentFromPid();
+        $controller->rootLine = $pageInformation->getRootLine();
+
+        // Update SYS_LASTCHANGED at the very last, when $this->page might be changed
+        // by settingLanguage() and the $this->page was finally resolved.
+        // Is also called when a translated page is in use, so the register reflects
+        // the state of the translated page, not the page in the default language.
+        $pageRecord = $pageInformation->getPageRecord();
+        $controller->register['SYS_LASTCHANGED'] = (int)$pageRecord['tstamp'];
+        if ($controller->register['SYS_LASTCHANGED'] < (int)$pageRecord['SYS_LASTCHANGED']) {
+            $controller->register['SYS_LASTCHANGED'] = (int)$pageRecord['SYS_LASTCHANGED'];
         }
+
         // Check if backend user has read access to this page.
         if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
             && $this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)
@@ -100,7 +168,7 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
             return GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
                 $request,
                 'ID was not an accessible page',
-                $controller->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
+                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
             );
         }
 
@@ -111,4 +179,602 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface
         $GLOBALS['TSFE'] = $controller;
         return $handler->handle($request);
     }
+
+    /**
+     * Set up proper PageInformation object later available as
+     * 'frontend.page.information' Request attribute.
+     *
+     * At this point, the Context object already contains relevant preview
+     * settings - e.g. if a backend user is logged in.
+     *
+     * @internal public since it is directly called as hack by RedirectService. Will change.
+     */
+    public function determineId(ServerRequestInterface $request, PageInformation $pageInformation): ?ResponseInterface
+    {
+        $event = $this->eventDispatcher->dispatch(new BeforePageIsResolvedEvent($request, $pageInformation));
+        $pageInformation = $event->getPageInformation();
+
+        $site = $request->getAttribute('site');
+
+        $this->timeTracker->push('determineId rootLine/');
+        try {
+            // Sets ->page and ->rootline information based on ->id. ->id may change during this operation.
+            // If the found Page ID is not within the site, then pageNotFound is set.
+            $this->getPageAndRootline($request, $pageInformation);
+            // Checks if the rootPageId of the site is in the resolved rootLine.
+            // This is necessary so that references to page-id's via ?id=123 from other sites are not possible.
+            $siteRootWithinRootlineFound = false;
+            $rootLine = $pageInformation->getRootLine();
+            foreach ($rootLine as $pageInRootLine) {
+                if ((int)$pageInRootLine['uid'] === $site->getRootPageId()) {
+                    $siteRootWithinRootlineFound = true;
+                    break;
+                }
+            }
+            // Page is 'not found' in case the id was outside the domain, code 3.
+            // This can only happen if there was a shortcut. So $pageInformation->pageRecord is now the shortcut target,
+            // but the original page is in $pageInformation->originalShortcutPageRecord. This only happens if people actually
+            // call TYPO3 with index.php?id=123 where 123 is in a different page tree, which is not allowed.
+            // Render the site root page instead.
+            $directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0);
+            if (!$siteRootWithinRootlineFound && $directlyRequestedId && (int)($pageInformation->getOriginalShortcutPageRecord()['uid'] ?? 0) !== $directlyRequestedId) {
+                $this->pageNotFound = 3;
+                $pageInformation->setId($site->getRootPageId());
+                // re-get the page and rootline if the id was not found.
+                $this->getPageAndRootline($request, $pageInformation);
+            }
+        } catch (ShortcutTargetPageNotFoundException) {
+            $this->pageNotFound = 1;
+        }
+        $this->timeTracker->pull();
+
+        $event = $this->eventDispatcher->dispatch(new AfterPageWithRootLineIsResolvedEvent($request, $pageInformation));
+        $pageInformation = $event->getPageInformation();
+        if ($event->getResponse()) {
+            return $event->getResponse();
+        }
+
+        $response = null;
+        try {
+            $this->evaluatePageNotFound($this->pageNotFound, $request);
+            // Setting language and fetch translated page
+            $pageInformation = $this->settingLanguage($request, $pageInformation);
+            // Check the "content_from_pid" field of the resolved page
+            $pageInformation->setContentFromPid($this->resolveContentPid($request, $pageInformation));
+        } catch (PropagateResponseException $e) {
+            $response = $e->getResponse();
+        }
+
+        $event = $this->eventDispatcher->dispatch(new AfterPageAndLanguageIsResolvedEvent($request, $pageInformation, $response));
+        $pageInformation = $event->getPageInformation();
+        // @todo: Change signature of method to always throw 'early' exceptions with response, if one is created.
+        //        Catch in calling method. After that, return the 'final' pageInformation object here instead
+        //        to follow 'normal' code flow.
+        return $event->getResponse();
+    }
+
+    /**
+     * Loads the page and root line records.
+     *
+     * A final page and the matching root line are determined and loaded by
+     * the algorithm defined by this method.
+     *
+     * First it loads the initial page from the page repository for given page uid.
+     * If that can't be loaded directly, it gets the root line for given page uid.
+     * It walks up the root line towards the root page until the page
+     * repository can deliver a page record. (The loading restrictions of
+     * the root line records are more liberal than that of the page record.)
+     *
+     * Now the page type is evaluated and handled if necessary. If the page is
+     * a shortcut, it is replaced by the target page. If the page is a mount
+     * point in overlay mode, the page is replaced by the mounted page.
+     *
+     * After this potential replacements are done, the root line is loaded
+     * (again) for this page record. It walks up the rootLine up to
+     * the first viewable record.
+     *
+     * While upon the first accessibility check of the root line it was done
+     * by loading page by page from the page repository, this time the method
+     * checkRootlineForIncludeSection() is used to find the most distant
+     * accessible page within the root line.
+     *
+     * Having found the final page id, the page record and the root line are
+     * loaded for last time by this method.
+     *
+     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
+     * or root lines.
+     *
+     * @throws ServiceUnavailableException
+     * @throws PageNotFoundException
+     * @throws ShortcutTargetPageNotFoundException
+     */
+    protected function getPageAndRootline(ServerRequestInterface $request, PageInformation $pageInformation): void
+    {
+        $requestedPageRowWithoutGroupCheck = [];
+        $id = $pageInformation->getId();
+        $pageRepository = $pageInformation->getPageRepository();
+        $mountPoint = $pageInformation->getMountPoint();
+        $pageRecord = $pageRepository->getPage($id);
+        $pageInformation->setPageRecord($pageRecord);
+        if (empty($pageRecord)) {
+            // If no page, we try to find the page above in the rootLine.
+            // Page is 'not found' in case the id itself was not an accessible page. code 1
+            $this->pageNotFound = 1;
+            $requestedPageIsHidden = false;
+            try {
+                $hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? '';
+                $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
+                if (!empty($hiddenField) && !$includeHiddenPages) {
+                    // Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays)
+                    $rawPageRecord = $pageRepository->getPage_noCheck($id);
+                    // If page record could not be resolved throw exception
+                    if ($rawPageRecord === []) {
+                        $message = 'The requested page does not exist!';
+                        try {
+                            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                                $request,
+                                $message,
+                                ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
+                            );
+                            throw new PropagateResponseException($response, 1674144383);
+                        } catch (PageNotFoundException) {
+                            throw new PageNotFoundException($message, 1674539331);
+                        }
+                    }
+                    $requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField];
+                }
+                $requestedPageRowWithoutGroupCheck = $pageRepository->getPage($id, true);
+                if (!empty($requestedPageRowWithoutGroupCheck)) {
+                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
+                }
+                $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get();
+                $pageInformation->setRootLine($rootLine);
+                if (!empty($rootLine)) {
+                    $c = count($rootLine) - 1;
+                    while ($c > 0) {
+                        // Add to page access failure history:
+                        $this->pageAccessFailureHistory['direct_access'][] = $rootLine[$c];
+                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
+                        $c--;
+                        $id = (int)$rootLine[$c]['uid'];
+                        $pageInformation->setId($id);
+                        $pageRecord = $pageRepository->getPage($id);
+                        $pageInformation->setPageRecord($pageRecord);
+                        if (!empty($pageRecord)) {
+                            break;
+                        }
+                    }
+                }
+                unset($rootLine);
+            } catch (RootLineException) {
+                // @todo: Empty for now, $pageInformation->rootLine will stay empty array. *Maybe* the try-catch could
+                //        be relocated around the RootlineUtility->get() call above, but it is currently unclear if
+                //        ErrorController->pageNotFoundAction() may eventually throw such exceptions as well?
+            }
+            if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($pageRecord))) {
+                // Error out if there is still no page record!
+                $message = 'The requested page does not exist!';
+                try {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        $message,
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
+                    );
+                    throw new PropagateResponseException($response, 1533931330);
+                } catch (PageNotFoundException) {
+                    throw new PageNotFoundException($message, 1301648780);
+                }
+            }
+        }
+
+        // Spacer and sysfolders are not accessible in frontend
+        $pageDoktype = (int)($pageRecord['doktype'] ?? 0);
+        $isSpacerOrSysfolder = $pageDoktype === PageRepository::DOKTYPE_SPACER || $pageDoktype === PageRepository::DOKTYPE_SYSFOLDER;
+        // Page itself is not accessible, but the parent page is a spacer/sysfolder
+        if ($isSpacerOrSysfolder && !empty($requestedPageRowWithoutGroupCheck)) {
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
+                    $request,
+                    'Subsection was found and not accessible',
+                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
+                );
+                throw new PropagateResponseException($response, 1633171038);
+            } catch (PageNotFoundException) {
+                throw new PageNotFoundException('Subsection was found and not accessible', 1633171172);
+            }
+        }
+        if ($isSpacerOrSysfolder) {
+            $message = 'The requested page does not exist!';
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                    $request,
+                    $message,
+                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
+                );
+                throw new PropagateResponseException($response, 1533931343);
+            } catch (PageNotFoundException) {
+                throw new PageNotFoundException($message, 1301648781);
+            }
+        }
+
+        // Is the ID a link to another page?
+        if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
+            // Clear mount point if page is a shortcut: If the shortcut goes to another page, we LEAVE the rootline which the MP expects.
+            $mountPoint = '';
+            $pageInformation->setMountPoint($mountPoint);
+            // Saving the page so that we can check later - when we know about languages - whether we took the correct shortcut
+            // or if a translation of the page overwrites the shortcut target, and we need to follow the new target.
+            $pageInformation = $this->settingLanguage($request, $pageInformation);
+            // Reset vars to new state that may have been created by settingLanguage()
+            $pageRepository = $pageInformation->getPageRepository();
+            $pageRecord = $pageInformation->getPageRecord();
+            $pageInformation->setOriginalShortcutPageRecord($pageRecord);
+            $pageRecord = $pageRepository->resolveShortcutPage($pageRecord, true);
+            $pageInformation->setPageRecord($pageRecord);
+            $id = (int)$pageRecord['uid'];
+            $pageInformation->setId($id);
+            $pageDoktype = (int)($pageRecord['doktype'] ?? 0);
+        }
+
+        // If the page is a mount point which should be overlaid with the contents of the mounted page,
+        // it must never be accessible directly, but only in the mount point context.
+        // We thus change the current page id.
+        if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $pageRecord['mount_pid_ol']) {
+            $originalMountPointPageRecord = $pageRecord;
+            $pageInformation->setOriginalMountPointPageRecord($pageRecord);
+            $pageRecord = $pageRepository->getPage($originalMountPointPageRecord['mount_pid']);
+            if (empty($pageRecord)) {
+                $message = 'This page (ID ' . $originalMountPointPageRecord['uid'] . ') is of type "Mount point" and '
+                    . 'mounts a page which is not accessible (ID ' . $originalMountPointPageRecord['mount_pid'] . ').';
+                throw new PageNotFoundException($message, 1402043263);
+            }
+            $pageInformation->setPageRecord($pageRecord);
+            // If the current page is a shortcut, the MP parameter will be replaced
+            if ($mountPoint === '' || !empty($pageInformation->getOriginalShortcutPageRecord())) {
+                $mountPoint = $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
+            } else {
+                $mountPoint = $mountPoint . ',' . $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
+            }
+            $pageInformation->setMountPoint($mountPoint);
+            $id = (int)$pageRecord['uid'];
+            $pageInformation->setId($id);
+        }
+
+        // Get rootLine and error out if it can not be retrieved.
+        try {
+            $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get();
+        } catch (RootLineException) {
+            $rootLine = [];
+        }
+        if (empty($rootLine)) {
+            $message = 'The requested page did not have a proper connection to the tree root!';
+            $context = ['pageId' => $id];
+            if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) {
+                $context['requestUrl'] = $normalizedParams->getRequestUrl();
+            }
+            $this->logger->error($message, $context);
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
+                    $request,
+                    $message,
+                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
+                );
+                throw new PropagateResponseException($response, 1533931350);
+            } catch (AbstractServerErrorException $e) {
+                $this->logger->error($message, ['exception' => $e]);
+                $exceptionClass = get_class($e);
+                throw new $exceptionClass($message, 1301648167);
+            }
+        }
+        $pageInformation->setRootLine($rootLine);
+
+        // Check for include section regarding hidden/starttime/endtime/fe_user - access control of a whole subbranch.
+        $updatedRootLine = $this->checkRootlineForIncludeSection($pageInformation);
+        if ($updatedRootLine !== null) {
+            if (empty($updatedRootLine)) {
+                $message = 'The requested page does not exist!';
+                try {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        $message,
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
+                    );
+                    throw new PropagateResponseException($response, 1533931351);
+                } catch (AbstractServerErrorException $e) {
+                    $this->logger->warning($message);
+                    $exceptionClass = get_class($e);
+                    throw new $exceptionClass($message, 1301648234);
+                }
+            }
+            $el = reset($updatedRootLine);
+            $id = (int)$el['uid'];
+            $pageInformation->setId($id);
+            $pageRecord = $pageRepository->getPage($id);
+            $pageInformation->setPageRecord($pageRecord);
+            try {
+                $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get();
+            } catch (RootLineException) {
+                $rootLine = [];
+            }
+            $pageInformation->setRootLine($rootLine);
+        }
+    }
+
+    /**
+     * If $this->pageNotFound is set, then throw an exception to stop further page generation process
+     */
+    protected function evaluatePageNotFound(int $pageNotFoundNumber, ServerRequestInterface $request): void
+    {
+        if (!$pageNotFoundNumber) {
+            return;
+        }
+        $response = match ($pageNotFoundNumber) {
+            1 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
+                $request,
+                'ID was not an accessible page',
+                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
+            ),
+            2 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
+                $request,
+                'Subsection was found and not accessible',
+                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
+            ),
+            3 => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                $request,
+                'ID was outside the domain',
+                $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
+            ),
+            default => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                $request,
+                'Unspecified error',
+                $this->getPageAccessFailureReasons()
+            ),
+        };
+        throw new PropagateResponseException($response, 1533931329);
+    }
+
+    /**
+     * Setting the language key that will be used by the current page.
+     * In this function it should be checked, 1) that this language exists, 2) that a page_overlay_record
+     * exists, and if not the default language, 0 (zero), should be set.
+     *
+     * May reset:
+     * $pageInformation->pageRecord
+     * $pageInformation->pageRepository
+     * $pageInformation->rootLine
+     */
+    protected function settingLanguage(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
+    {
+        // Get values from site language
+        $site = $request->getAttribute('site');
+        $language = $request->getAttribute('language', $site->getDefaultLanguage());
+        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($language);
+        $languageId = $languageAspect->getId();
+        $languageContentId = $languageAspect->getContentId();
+
+        $pageRecord = $pageInformation->getPageRecord();
+        $pageRepository = $pageInformation->getPageRepository();
+
+        $pageTranslationVisibility = new PageTranslationVisibility((int)($pageRecord['l18n_cfg'] ?? 0));
+        // If the incoming language is set to another language than default
+        if ($languageAspect->getId() > 0) {
+            // Request the translation for the requested language
+            $olRec = $pageRepository->getPageOverlay($pageRecord, $languageAspect);
+            $overlaidLanguageId = (int)($olRec['sys_language_uid'] ?? 0);
+            if ($overlaidLanguageId !== $languageAspect->getId()) {
+                // If requested translation is not available
+                if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'Page is not available in the requested language.',
+                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
+                    );
+                    throw new PropagateResponseException($response, 1533931388);
+                }
+                switch ($languageAspect->getLegacyLanguageMode()) {
+                    case 'strict':
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                            $request,
+                            'Page is not available in the requested language (strict).',
+                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
+                        );
+                        throw new PropagateResponseException($response, 1533931395);
+                    case 'content_fallback':
+                        // Setting content uid (but leaving the sys_language_uid) when a content_fallback value was found.
+                        foreach ($languageAspect->getFallbackChain() as $orderValue) {
+                            if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
+                                $languageContentId = 0;
+                                break;
+                            }
+                            if (MathUtility::canBeInterpretedAsInteger($orderValue) && $overlaidLanguageId === (int)$orderValue) {
+                                $languageContentId = (int)$orderValue;
+                                break;
+                            }
+                            if ($orderValue === 'pageNotFound') {
+                                // The existing fallbacks have not been found, but instead of continuing page rendering
+                                // with default language, a "page not found" message should be shown instead.
+                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                                    $request,
+                                    'Page is not available in the requested language (fallbacks did not apply).',
+                                    ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
+                                );
+                                throw new PropagateResponseException($response, 1533931402);
+                            }
+                        }
+                        break;
+                    default:
+                        // Default is that everything defaults to the default language...
+                        $languageId = ($languageContentId = 0);
+                }
+            }
+
+            // Define the language aspect again now
+            $languageAspect = GeneralUtility::makeInstance(
+                LanguageAspect::class,
+                $languageId,
+                $languageContentId,
+                $languageAspect->getOverlayType(),
+                $languageAspect->getFallbackChain()
+            );
+
+            // Setting localized page record if an overlay record was found (which it is only if a language is used)
+            // Doing this ensures that page properties like the page title are resolved in the correct language
+            $pageInformation->setPageRecord($olRec);
+        }
+
+        // Set the language aspect
+        $this->context->setAspect('language', $languageAspect);
+        // Setting sys_language_uid inside sys-page by creating a new page repository
+        $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class));
+        // If default language is not available
+        if ((!$languageAspect->getContentId() || !$languageAspect->getId())
+            && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
+        ) {
+            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                $request,
+                'Page is not available in default language.',
+                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
+            );
+            throw new PropagateResponseException($response, 1533931423);
+        }
+
+        if ($languageAspect->getId() > 0) {
+            // Updating rootLine with translations if the language key is set.
+            try {
+                $pageInformation->setRootLine(GeneralUtility::makeInstance(
+                    RootlineUtility::class,
+                    $pageInformation->getId(),
+                    $pageInformation->getMountPoint()
+                )->get());
+            } catch (RootLineException) {
+                $pageInformation->setRootLine([]);
+            }
+        }
+
+        return $pageInformation;
+    }
+
+    /**
+     * Check the value of "content_from_pid" of the current page record, and see if the current request
+     * should actually show content from another page.
+     *
+     * @return int the current page ID or another one if resolved properly
+     */
+    protected function resolveContentPid(ServerRequestInterface $request, PageInformation $pageInformation): int
+    {
+        $pageRecord = $pageInformation->getPageRecord();
+        if (empty($pageRecord['content_from_pid'])) {
+            return $pageInformation->getId();
+        }
+        // @todo: This does *not* re-init $pageInformation->pageRepository.
+        //        It is currently unclear if this has positive or negative side effects!
+        $pageInformation = clone $pageInformation;
+        // Set id to the content_from_pid value - we are going to evaluate this pid as if it was a given id for a page-display.
+        $pageInformation->setId($pageRecord['content_from_pid']);
+        $pageInformation->setMountPoint('');
+        $this->getPageAndRootline($request, $pageInformation);
+        return $pageInformation->getId();
+    }
+
+    /**
+     * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions.
+     * That data can be used inside a page-not-found handler
+     *
+     * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
+     * @return array Summary of why page access was not allowed.
+     */
+    protected function getPageAccessFailureReasons(string $failureReasonCode = null): array
+    {
+        $output = [];
+        if ($failureReasonCode) {
+            $output['code'] = $failureReasonCode;
+        }
+        $combinedRecords = array_merge(
+            is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]],
+            is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : []
+        );
+        if (!empty($combinedRecords)) {
+            $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
+            foreach ($combinedRecords as $k => $pagerec) {
+                // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course
+                // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the
+                // extendToSubpages flag set, hence checked only then!
+                if (!$k || $pagerec['extendToSubpages']) {
+                    if ($pagerec['hidden'] ?? false) {
+                        $output['hidden'][$pagerec['uid']] = true;
+                    }
+                    if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
+                        $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
+                    }
+                    if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
+                        $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
+                    }
+                    if (!$accessVoter->groupAccessGranted('pages', $pagerec, $this->context)) {
+                        $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
+                    }
+                }
+            }
+        }
+        return $output;
+    }
+
+    /**
+     * Checks if visibility of the page is blocked upwards in the root line.
+     *
+     * If any page in the root line is blocking visibility, true is returned.
+     *
+     * All pages from the blocking page downwards are removed from the root
+     * line, so that the remaining pages can be used to relocate the page up
+     * to the lowest visible page.
+     *
+     * The blocking feature of a page must be turned on by setting the page
+     * record field 'extendToSubpages' to 1 in case of hidden, starttime,
+     * endtime or fe_group restrictions.
+     *
+     * Additionally, this method checks for backend user sections in root line
+     * and if found, evaluates if a backend user is logged in and has access.
+     */
+    protected function checkRootlineForIncludeSection(PageInformation $pageInformation): ?array
+    {
+        $rootLine = $pageInformation->getRootLine();
+        $pageRecord = $pageInformation->getPageRecord();
+        $c = count($rootLine);
+        $removeTheRestFlag = false;
+        $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
+        for ($a = 0; $a < $c; $a++) {
+            if (!$accessVoter->accessGrantedForPageInRootLine($rootLine[$a], $this->context)) {
+                // Add to page access failure history and mark the page as not found.
+                // Keep the rootLine however to trigger access denied error instead of a service unavailable error
+                $this->pageAccessFailureHistory['sub_section'][] = $rootLine[$a];
+                $this->pageNotFound = 2;
+            }
+            if ((int)$rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
+                // If there is a backend user logged in, check if they have read access to the page
+                if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
+                    // If there was no page selected, the user apparently did not have read access to the
+                    // current page (not position in rootLine) and we set the remove-flag...
+                    if (!$this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW)) {
+                        $removeTheRestFlag = true;
+                    }
+                } else {
+                    // Don't go here, if there is no backend user logged in.
+                    $removeTheRestFlag = true;
+                }
+            }
+            if ($removeTheRestFlag) {
+                // Page is 'not found' in case a subsection was found and not accessible, code 2
+                $this->pageNotFound = 2;
+                unset($rootLine[$a]);
+            }
+        }
+        if ($removeTheRestFlag) {
+            return $rootLine;
+        }
+        return null;
+    }
+
+    protected function getBackendUser(): ?FrontendBackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'] ?? null;
+    }
 }
diff --git a/typo3/sysext/frontend/Classes/Page/PageInformation.php b/typo3/sysext/frontend/Classes/Page/PageInformation.php
new file mode 100644
index 0000000000000000000000000000000000000000..6275be971ba8cf2584267549dcad9712d2acb785
--- /dev/null
+++ b/typo3/sysext/frontend/Classes/Page/PageInformation.php
@@ -0,0 +1,195 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Frontend\Page;
+
+use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+
+/**
+ * This DTO carries various Frontend rendering related page information. It is
+ * set up by a Frontend middleware and attached to as 'frontend.page.information'
+ * Request attribute.
+ *
+ * @internal Still experimental
+ */
+final class PageInformation
+{
+    private int $id;
+    private PageRepository $pageRepository;
+    private array $pageRecord;
+    private string $mountPoint = '';
+    private int $contentFromPid;
+
+    /**
+     * Gets set when we are processing a page of type shortcut in the early stages
+     * of the request, used later in a middleware to resolve the shortcut and redirect again.
+     */
+    private ?array $originalShortcutPageRecord = null;
+
+    /**
+     * Gets set when we are processing a page of type mountpoint with enabled overlay in getPageAndRootline()
+     * Used later in a middleware to determine the final target URL where the user should be redirected to.
+     */
+    private ?array $originalMountPointPageRecord = null;
+
+    /**
+     * Rootline of page records all the way to the root.
+     *
+     * Both language and version overlays are applied to these page records:
+     * All "data" fields are set to language / version overlay values, *except* uid and
+     * pid, which are the default-language and live-version ids.
+     *
+     * First array row with the highest key is the deepest page (the requested page),
+     * then parent pages with descending keys until (but not including) the
+     * project root pseudo page 0.
+     *
+     * When page uid 5 is called in this example:
+     * [0] Project name
+     * |- [2] An organizational page, probably with is_siteroot=1 and a site config
+     *    |- [3] Site root with a sys_template having "root" flag set
+     *       |- [5] Here you are
+     *
+     * This $absoluteRootLine is:
+     * [3] => [uid = 5, pid = 3, title = Here you are, ...]
+     * [2] => [uid = 3, pid = 2, title = Site root with a sys_template having "root" flag set, ...]
+     * [1] => [uid = 2, pid = 0, title = An organizational page, probably with is_siteroot=1 and a site config, ...]
+     *
+     * Read-only! Extensions may read but never write this property!
+     *
+     * @var array<int, array<string, mixed>>
+     */
+    private array $rootLine = [];
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setId(int $id): void
+    {
+        $this->id = $id;
+    }
+
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setPageRepository(PageRepository $pageRepository): void
+    {
+        $this->pageRepository = $pageRepository;
+    }
+
+    /**
+     * @internal Only to be read by core
+     */
+    public function getPageRepository(): PageRepository
+    {
+        return $this->pageRepository;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setPageRecord(array $pageRecord): void
+    {
+        $this->pageRecord = $pageRecord;
+    }
+
+    public function getPageRecord(): array
+    {
+        return $this->pageRecord;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setMountPoint(string $mountPoint): void
+    {
+        $this->mountPoint = $mountPoint;
+    }
+
+    /**
+     * @internal Only to be read by core
+     */
+    public function getMountPoint(): string
+    {
+        return $this->mountPoint;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setRootLine(array $rootLine): void
+    {
+        $this->rootLine = $rootLine;
+    }
+
+    public function getRootLine(): array
+    {
+        return $this->rootLine;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setOriginalShortcutPageRecord(array $originalShortcutPageRecord): void
+    {
+        $this->originalShortcutPageRecord = $originalShortcutPageRecord;
+    }
+
+    /**
+     * @internal Only to be read by core
+     */
+    public function getOriginalShortcutPageRecord(): ?array
+    {
+        return $this->originalShortcutPageRecord;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setOriginalMountPointPageRecord(array $originalMountPointPageRecord): void
+    {
+        $this->originalMountPointPageRecord = $originalMountPointPageRecord;
+    }
+
+    /**
+     * @internal Only to be read by core
+     */
+    public function getOriginalMountPointPageRecord(): ?array
+    {
+        return $this->originalMountPointPageRecord;
+    }
+
+    /**
+     * @internal Only to be set by core
+     */
+    public function setContentFromPid(int $contentFromPid): void
+    {
+        $this->contentFromPid = $contentFromPid;
+    }
+
+    /**
+     * @internal Only to be read by core
+     */
+    public function getContentFromPid(): int
+    {
+        return $this->contentFromPid;
+    }
+}
diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml
index 18d8e0c8ae6c27dd84312000a2239326096dc167..9b76fa7d995d0f9dc7f20a155b51e2756f6542e4 100644
--- a/typo3/sysext/frontend/Configuration/Services.yaml
+++ b/typo3/sysext/frontend/Configuration/Services.yaml
@@ -15,6 +15,10 @@ services:
     arguments:
       $cache: '@cache.assets'
 
+  # @todo: Remove "shared: false" again when this middleware is stateless
+  TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization:
+    shared: false
+
   TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor:
     public: true
 
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index d9a21846c45a6b28656d8a72ae00dcf1402bdba5..4b7d68a9ef4e0d2a19262d98d9290272e5050fb2 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -6255,4 +6255,25 @@ return [
             'Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst',
         ],
     ],
+    'TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent->getController' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent->getController' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent->getController' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php
index 1bedbb7197c59587f07d50b9ed94d1da68509b8d..081e1f91c736a71ba4fd09fc94e116052815f22a 100644
--- a/typo3/sysext/redirects/Classes/Service/RedirectService.php
+++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php
@@ -39,6 +39,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Cache\CacheInstruction;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization;
+use TYPO3\CMS\Frontend\Page\PageInformation;
 use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
 use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
 use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent;
@@ -389,14 +391,29 @@ class RedirectService implements LoggerAwareInterface
     {
         $cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction());
         $originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction);
+        $pageArguments = new PageArguments($site->getRootPageId(), '0', []);
+        $pageInformation = new PageInformation();
+        $pageInformation->setId($site->getRootPageId());
+        $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class));
+        $pageInformation->setMountPoint('');
+        $tsfeInitMiddleware = GeneralUtility::makeInstance(TypoScriptFrontendInitialization::class);
+        // @todo: Evil hack.
+        $tsfeInitMiddleware->determineId($originalRequest, $pageInformation);
+        $originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation);
         $controller = GeneralUtility::makeInstance(
             TypoScriptFrontendController::class,
             GeneralUtility::makeInstance(Context::class),
             $site,
             $site->getDefaultLanguage(),
-            new PageArguments($site->getRootPageId(), '0', [])
+            $pageArguments
         );
-        $controller->determineId($originalRequest);
+        // b/w compat layer
+        $controller->id = $pageInformation->getId();
+        $controller->sys_page = $pageInformation->getPageRepository();
+        $controller->page = $pageInformation->getPageRecord();
+        $controller->MP = $pageInformation->getMountPoint();
+        $controller->contentPid = $pageInformation->getContentFromPid();
+        $controller->rootLine = $pageInformation->getRootLine();
         $controller->calculateLinkVars($queryParams);
         $newRequest = $controller->getFromCache($originalRequest);
         $controller->releaseLocks();
@@ -404,9 +421,6 @@ class RedirectService implements LoggerAwareInterface
         if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
             $GLOBALS['TSFE'] = $controller;
         }
-        if (!$GLOBALS['TSFE']->sys_page instanceof PageRepository) {
-            $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class);
-        }
         return $controller;
     }