diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 79cf0becf0acc08ff5851e6cb8a99a2ac0567a31..f3e705bce1e0f4a1a3cba4f5cae7faec942528ca 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -9179,7 +9179,7 @@ class DataHandler implements LoggerAwareInterface
      * Clears cache for the page pointed to by $cacheCmd (an integer).
      *
      * $cacheCmd='cacheTag:[string]'
-     * Flush page and pagesection cache by given tag
+     * Flush page cache by given tag
      *
      * $cacheCmd='cacheId:[string]'
      * Removes cache identifier from page and page section cache
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..bad16bea0dd1bedd19d263ae83a6af58bc0ea2f2
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php
@@ -0,0 +1,55 @@
+<?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\Core\TypoScript\IncludeTree\Event;
+
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+
+/**
+ * A PSR-14 event fired when sys_template rows have been fetched.
+ *
+ * This event is intended to add own rows based on given rows or site resolution.
+ */
+final class AfterTemplatesHaveBeenDeterminedEvent
+{
+    public function __construct(
+        private readonly array $rootline,
+        private readonly SiteInterface $site,
+        private array $templateRows,
+    ) {
+    }
+
+    public function getRootline(): array
+    {
+        return $this->rootline;
+    }
+
+    public function getSite(): SiteInterface
+    {
+        return $this->site;
+    }
+
+    public function getTemplateRows(): array
+    {
+        return $this->templateRows;
+    }
+
+    public function setTemplateRows(array $templateRows): void
+    {
+        $this->templateRows = $templateRows;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..2e2d5894cd9d6f0bfb21539d66aa26e3fe665f6a
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php
@@ -0,0 +1,199 @@
+<?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\Core\TypoScript\IncludeTree;
+
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
+use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Fetch relevant sys_template records from database by given page rootline.
+ *
+ * The result sys_template rows are fed to the TreeBuilder for processing.
+ *
+ * @internal: Internal structure. There is optimization potential and especially getSysTemplateRowsByRootline() will probably vanish later.
+ */
+final class SysTemplateRepository
+{
+    public function __construct(
+        private readonly EventDispatcherInterface $eventDispatcher,
+        private readonly ConnectionPool $connectionPool,
+        private readonly Context $context,
+    ) {
+    }
+
+    /**
+     * To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages.
+     * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting
+     * value.
+     * The query implementation below does that with *one* query for all rootline pages at once, not
+     * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but
+     * the implementation should scale nearly O(1) instead of O(n) with the rootline depth.
+     *
+     * @todo: It's potentially possible to get rid of this method in the frontend by joining sys_template
+     *        into the Page rootline resolving as soon as it uses a CTE.
+     */
+    public function getSysTemplateRowsByRootline(array $rootline, SiteInterface $site): array
+    {
+        // Site-root node first!
+        $rootLinePageIds = array_reverse(array_column($rootline, 'uid'));
+        $sysTemplateRows = [];
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
+        $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer());
+        $queryBuilder->select('sys_template.*')->from('sys_template');
+        // Build a value list as joined table to have sorting based on list sorting
+        $valueList = [];
+        // @todo: Use type/int cast from expression builder to handle this dbms aware
+        //        when support for this has been extracted from CTE PoC patch (sbuerk).
+        $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform;
+        $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting';
+        foreach ($rootLinePageIds as $sorting => $rootLinePageId) {
+            $valueList[] = sprintf(
+                $pattern,
+                $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT),
+                $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT)
+            );
+        }
+        $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList);
+        $queryBuilder->getConcreteQueryBuilder()->innerJoin(
+            $queryBuilder->quoteIdentifier('sys_template'),
+            sprintf('(%s)', $valueList),
+            $queryBuilder->quoteIdentifier('pidlist'),
+            '(' . $queryBuilder->expr()->eq(
+                'sys_template.pid',
+                $queryBuilder->quoteIdentifier('pidlist.uid')
+            ) . ')'
+        );
+        // Sort by rootline determined depth as sort criteria
+        $queryBuilder->orderBy('pidlist.sorting', 'ASC')
+            ->addOrderBy('sys_template.root', 'DESC')
+            ->addOrderBy('sys_template.sorting', 'ASC');
+        $lastPid = null;
+        $queryResult = $queryBuilder->executeQuery();
+        while ($sysTemplateRow = $queryResult->fetchAssociative()) {
+            // We're retrieving *all* templates per pid, but need the first one only. The
+            // order restriction above at least takes care they're after-each-other per pid.
+            if ($lastPid === (int)$sysTemplateRow['pid']) {
+                continue;
+            }
+            $lastPid = (int)$sysTemplateRow['pid'];
+            $sysTemplateRows[] = $sysTemplateRow;
+        }
+        $event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $site, $sysTemplateRows);
+        $this->eventDispatcher->dispatch($event);
+        return $event->getTemplateRows();
+    }
+
+    /**
+     * To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages.
+     * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting
+     * value.
+     *
+     * This variant is tailored for ext:tstemplate use. It allows "overriding" the sys_template uid of
+     * the deepest page, which is used when multiple sys_template records on one page are managed in the Backend.
+     *
+     * The query implementation below does that with *one* query for all rootline pages at once, not
+     * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but
+     * the implementation should scale nearly O(1) instead of O(n) with the rootline depth.
+     */
+    public function getSysTemplateRowsByRootlineWithUidOverride(array $rootline, SiteInterface $site, int $templateUidOnDeepestRootline): array
+    {
+        // Site-root node first!
+        $rootLinePageIds = array_reverse(array_column($rootline, 'uid'));
+        $templatePidOnDeepestRootline = $rootline[array_key_first($rootline)]['uid'];
+        $sysTemplateRows = [];
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
+        $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer());
+        $queryBuilder->select('sys_template.*')->from('sys_template');
+        if ($templateUidOnDeepestRootline && $templatePidOnDeepestRootline) {
+            $queryBuilder->andWhere(
+                $queryBuilder->expr()->or(
+                    $queryBuilder->expr()->neq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)),
+                    $queryBuilder->expr()->and(
+                        $queryBuilder->expr()->eq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)),
+                        $queryBuilder->expr()->eq('sys_template.uid', $queryBuilder->createNamedParameter($templateUidOnDeepestRootline, \PDO::PARAM_INT)),
+                    ),
+                ),
+            );
+        }
+        // Build a value list as joined table to have sorting based on list sorting
+        $valueList = [];
+        // @todo: Use type/int cast from expression builder to handle this dbms aware
+        //        when support for this has been extracted from CTE PoC patch (sbuerk).
+        $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform;
+        $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting';
+        foreach ($rootLinePageIds as $sorting => $rootLinePageId) {
+            $valueList[] = sprintf(
+                $pattern,
+                $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT),
+                $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT)
+            );
+        }
+        $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList);
+        $queryBuilder->getConcreteQueryBuilder()->innerJoin(
+            $queryBuilder->quoteIdentifier('sys_template'),
+            sprintf('(%s)', $valueList),
+            $queryBuilder->quoteIdentifier('pidlist'),
+            '(' . $queryBuilder->expr()->eq(
+                'sys_template.pid',
+                $queryBuilder->quoteIdentifier('pidlist.uid')
+            ) . ')'
+        );
+        // Sort by rootline determined depth as sort criteria
+        $queryBuilder->orderBy('pidlist.sorting', 'ASC')
+            ->addOrderBy('sys_template.root', 'DESC')
+            ->addOrderBy('sys_template.sorting', 'ASC');
+        $lastPid = null;
+        $queryResult = $queryBuilder->executeQuery();
+        while ($sysTemplateRow = $queryResult->fetchAssociative()) {
+            // We're retrieving *all* templates per pid, but need the first one only. The
+            // order restriction above at least takes care they're after-each-other per pid.
+            if ($lastPid === (int)$sysTemplateRow['pid']) {
+                continue;
+            }
+            $lastPid = (int)$sysTemplateRow['pid'];
+            $sysTemplateRows[] = $sysTemplateRow;
+        }
+        // @todo: This event should be able to be fired even if the sys_template resolving is
+        //        merged into an early middleware like "SiteResolver" which could join / sub-select
+        //        pages together with sys_template directly, which would be possible if we manage
+        //        to switch away from RootlineUtility usage in SiteResolver by using a CTE instead.
+        $event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $site, $sysTemplateRows);
+        $this->eventDispatcher->dispatch($event);
+        return $event->getTemplateRows();
+    }
+
+    /**
+     * Get sys_template record query builder restrictions.
+     * Allows hidden records if enabled in context.
+     */
+    private function getSysTemplateQueryRestrictionContainer(): DefaultRestrictionContainer
+    {
+        $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
+        if ($this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)) {
+            $restrictionContainer->removeByType(HiddenRestriction::class);
+        }
+        return $restrictionContainer;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php
index c26593baee9fcc06d161c9bba92ae8db3cb30ed4..d7fc5cec2659484e6484b022046b61f0d8b547ad 100644
--- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php
@@ -17,18 +17,15 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\TypoScript\IncludeTree;
 
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
-use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
-use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptInclude;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptMagicKeyInclude;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ExtensionStaticInclude;
@@ -45,7 +42,6 @@ use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * Create a tree representing all TypoScript includes.
@@ -102,30 +98,14 @@ final class TreeBuilder
      */
     private array $includedSysTemplateUids = [];
 
-    /**
-     * Site of given rootline if possible. Used to resolve site based default constants.
-     */
-    private ?SiteInterface $site = null;
-
     /**
      * Either 'constants' or 'setup'
      */
     private string $type;
 
-    /**
-     * To calculate full setup TypoScript, this class needs to be called twice: Once to retrieve
-     * "constants", and a second time to retrieve "setup" include tree. To suppress identical DB
-     * calls for the second cycle, getTreeByRootline() can be called with $cached argument.
-     */
-    private bool $cached;
-
-    private array $sysTemplateRows;
-    private array $basedOnTemplateRows;
-
     public function __construct(
         private readonly ConnectionPool $connectionPool,
         private readonly PackageManager $packageManager,
-        private readonly SiteFinder $siteFinder,
         private readonly Context $context,
         private readonly TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
         private TokenizerInterface $tokenizer,
@@ -141,10 +121,19 @@ final class TreeBuilder
     public function setTokenizer(TokenizerInterface $tokenizer): void
     {
         $this->tokenizer = $tokenizer;
-        $this->cache = null;
+        $this->disableCache();
         $this->treeFromTokenStreamBuilder->setTokenizer($tokenizer);
     }
 
+    /**
+     * Used in FE in no_cache = true context.
+     * @todo: Maybe change this and simply hand over a cache to getTreeBySysTemplateRowsAndSite() when needed?
+     */
+    public function disableCache(): void
+    {
+        $this->cache = null;
+    }
+
     /**
      * This is a special case for extbase BE modules and should not be used otherwise. See property comment.
      */
@@ -153,32 +142,16 @@ final class TreeBuilder
         $this->forceProcessExtensionStatics = true;
     }
 
-    /**
-     * @param array $rootline The "reversed" rootline as coming from RootlineUtility: Deepest page with
-     *                        the highest key as first entry, site-root page with key 0 as last entry.
-     */
-    public function getTreeByRootline(array $rootline, string $type, bool $cached, int $templateUidOnDeepestRootline = 0): RootInclude
+    public function getTreeBySysTemplateRowsAndSite(string $type, array $sysTemplateRows, ?SiteInterface $site = null): RootInclude
     {
         if (!in_array($type, ['constants', 'setup'])) {
             throw new \RuntimeException('type must be either constants or setup', 1653737656);
         }
 
         $this->type = $type;
-        $this->cached = $cached;
         $this->includedSysTemplateUids = [];
         $this->extensionStaticsProcessed = false;
 
-        if ($cached) {
-            // Note this fatales if calling getTreeByRootline() with $cached=true when it has not
-            // been called with $cached=false before. This is intended: We don't need check-code if
-            // it simply fatales on broken use.
-            $sysTemplateRows = $this->sysTemplateRows;
-        } else {
-            $this->site = $this->determineSite($rootline);
-            $sysTemplateRows = $this->getRootlineSysTemplateRowsFromDatabase($rootline, $templateUidOnDeepestRootline);
-            $this->sysTemplateRows = $sysTemplateRows;
-        }
-
         $includeTree = new RootInclude();
 
         foreach ($sysTemplateRows as $sysTemplateRow) {
@@ -210,21 +183,22 @@ final class TreeBuilder
             ) {
                 $includeNode->setClear(true);
             }
-            $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow);
+            $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site);
             $this->treeFromTokenStreamBuilder->buildTree($includeNode, $this->type);
             $this->cache?->set($identifier, $this->prepareNodeForCache($includeNode));
             $includeTree->addChild($includeNode);
         }
 
-        // @todo: b/w compat hook hack tailored for testing-framework TyposcriptInstruction runThroughTemplatesPostProcessing
-        //        hook. Substitute with an event and look at usages like ext:bolt when doing this.
-        //        Note we also don't cache this hook result, which we either won't want at all and rely on "sub-caches" by
-        //        TreeFromLineStreamBuilder, or implement it? Unsure.
+        // @todo: b/w compat hook hack tailored for testing-framework TyposcriptInstruction runThroughTemplatesPostProcessing hook.
+        //        This hook has already been marked as removed in v12. We should drop it without further notice in v12
+        //        stabilization phase and make TF cope with it, probably by switching to AfterTemplatesHaveBeenDeterminedEvent.
+        // @deprecated hook runThroughTemplatesPostProcessing, will vanish in v12.
         $hookParameters = [];
         $templateService = new TemplateService();
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing'] ?? [] as $listener) {
             GeneralUtility::callUserFunction($listener, $hookParameters, $templateService);
             if (!empty($templateService->constants)) {
+                // @todo: Aehm, we should check $this->type here, shouldn't we?
                 $node = new DefaultTypoScriptInclude();
                 $node->setIdentifier('hook-constants');
                 $node->setName('Hook constants');
@@ -232,6 +206,7 @@ final class TreeBuilder
                 $includeTree->addChild($node);
             }
             if (!empty($templateService->config)) {
+                // @todo: Aehm, we should check $this->type here, shouldn't we?
                 $node = new DefaultTypoScriptInclude();
                 $node->setIdentifier('hook-setup');
                 $node->setName('Hook setup');
@@ -244,7 +219,7 @@ final class TreeBuilder
             // Extbase hack: See property description above.
             if ($this->type === 'constants') {
                 $this->addDefaultTypoScriptFromGlobals($includeTree);
-                $this->addDefaultTypoScriptConstantsFromSite($includeTree);
+                $this->addDefaultTypoScriptConstantsFromSite($includeTree, $site);
             } else {
                 $this->addDefaultTypoScriptFromGlobals($includeTree);
             }
@@ -257,7 +232,7 @@ final class TreeBuilder
     /**
      * Add includes defined in a sys_template record.
      */
-    private function handleSysTemplateRecordInclude(IncludeInterface $parentNode, array $row): void
+    private function handleSysTemplateRecordInclude(IncludeInterface $parentNode, array $row, ?SiteInterface $site): void
     {
         $this->includedSysTemplateUids[] = (int)$row['uid'];
 
@@ -269,7 +244,7 @@ final class TreeBuilder
 
         if ($this->type === 'constants' && $clearConstants) {
             $this->addDefaultTypoScriptFromGlobals($parentNode);
-            $this->addDefaultTypoScriptConstantsFromSite($parentNode);
+            $this->addDefaultTypoScriptConstantsFromSite($parentNode, $site);
         }
         if ($this->type === 'setup' && $clearSetup) {
             $this->addDefaultTypoScriptFromGlobals($parentNode);
@@ -281,7 +256,7 @@ final class TreeBuilder
             $this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']);
         }
         if (!empty($row['basedOn'])) {
-            $this->handleIncludeBasedOnTemplates($parentNode, (string)$row['basedOn']);
+            $this->handleIncludeBasedOnTemplates($parentNode, (string)$row['basedOn'], $site);
         }
         if ($includeStaticAfterBasedOn) {
             $this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']);
@@ -330,7 +305,7 @@ final class TreeBuilder
      * Warning: Calls handleSysTemplateRecordInclude() recursive when another basedOn templates
      *          record includes things again!
      */
-    private function handleIncludeBasedOnTemplates(IncludeInterface $parentNode, string $basedOnList): void
+    private function handleIncludeBasedOnTemplates(IncludeInterface $parentNode, string $basedOnList, ?SiteInterface $site): void
     {
         $basedOnTemplateUids = GeneralUtility::intExplode(',', $basedOnList, true);
         // Filter uids that have been handled already.
@@ -339,12 +314,7 @@ final class TreeBuilder
             return;
         }
 
-        if ($this->cached) {
-            $basedOnTemplateRows = $this->basedOnTemplateRows[implode('-', $basedOnTemplateUids)];
-        } else {
-            $basedOnTemplateRows = $this->getBasedOnSysTemplateRowsFromDatabase($basedOnTemplateUids);
-            $this->basedOnTemplateRows[implode('-', $basedOnTemplateUids)] = $basedOnTemplateRows;
-        }
+        $basedOnTemplateRows = $this->getBasedOnSysTemplateRowsFromDatabase($basedOnTemplateUids);
 
         foreach ($basedOnTemplateUids as $basedOnTemplateUid) {
             if (is_array($basedOnTemplateRows[$basedOnTemplateUid] ?? false)) {
@@ -372,7 +342,7 @@ final class TreeBuilder
                     $includeNode->setClear(true);
                 }
                 $parentNode->addChild($includeNode);
-                $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow);
+                $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site);
             }
         }
     }
@@ -509,13 +479,13 @@ final class TreeBuilder
     /**
      * Load default TS constants from site configuration if that page has a site in rootline.
      */
-    private function addDefaultTypoScriptConstantsFromSite(IncludeInterface $parentConstantNode): void
+    private function addDefaultTypoScriptConstantsFromSite(IncludeInterface $parentConstantNode, ?SiteInterface $site): void
     {
-        if (!$this->site instanceof Site) {
+        if (!$site instanceof Site) {
             return;
         }
         $siteConstants = '';
-        $siteSettings = $this->site->getConfiguration()['settings'] ?? [];
+        $siteSettings = $site->getConfiguration()['settings'] ?? [];
         if (empty($siteSettings)) {
             return;
         }
@@ -533,7 +503,7 @@ final class TreeBuilder
         }
         $node = new SiteInclude();
         $node->setIdentifier($identifier);
-        $node->setName('Site constants settings of site ' . $this->site->getIdentifier());
+        $node->setName('Site constants settings of site ' . $site->getIdentifier());
         $node->setLineStream($this->tokenizer->tokenize($siteConstants));
         $this->cache?->set($identifier, $this->prepareNodeForCache($node));
         $parentConstantNode->addChild($node);
@@ -569,103 +539,6 @@ final class TreeBuilder
         }
     }
 
-    /**
-     * Note this takes the rootline array from 'lowest' up to page tree root: Deepest page
-     * first on key 0, higher page 1, and so on.
-     */
-    private function determineSite(array $rootline): ?SiteInterface
-    {
-        if ($this->getTypoScriptFrontendController() instanceof TypoScriptFrontendController) {
-            return $this->getTypoScriptFrontendController()->getSite();
-        }
-        $possibleRoots = array_filter($rootline, static function (array $page) {
-            return $page['is_siteroot'] === 1;
-        });
-        $possibleRoots[] = end($rootline);
-        foreach ($possibleRoots as $possibleRoot) {
-            try {
-                return $this->siteFinder->getSiteByPageId((int)($possibleRoot['uid'] ?? 0));
-            } catch (SiteNotFoundException $_) {
-                // continue
-            }
-        }
-        return null;
-    }
-
-    /**
-     * getTreeByRootline() receives the rootline of a page. To calculate the TS include tree, we have
-     * to find sys_template rows attached to all rootline pages.
-     * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting
-     * value. Additionally, the backend 'template' module allows selecting a sys_template record on the
-     * deepest page, if there is more than one.
-     * The query implementation below does that with *one* query for all rootline pages at once, not
-     * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but
-     * the implementation should scale nearly O(1) instead of O(n) with the rootline depth.
-     *
-     * @todo: It's potentially possible to further optimize using a recursive CTE. Benefit
-     *        won't be *that* huge though, and there are much more important CTE targets first.
-     */
-    private function getRootlineSysTemplateRowsFromDatabase(array $rootline, int $templateUidOnDeepestRootline): array
-    {
-        // Site-root node first!
-        $rootLinePageIds = array_reverse(array_column($rootline, 'uid'));
-        $templatePidOnDeepestRootline = $rootline[array_key_first($rootline)]['uid'];
-        $sysTemplateRows = [];
-        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
-        $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer());
-        $queryBuilder->select('sys_template.*')->from('sys_template');
-        if ($templateUidOnDeepestRootline && $templatePidOnDeepestRootline) {
-            $queryBuilder->andWhere(
-                $queryBuilder->expr()->or(
-                    $queryBuilder->expr()->neq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)),
-                    $queryBuilder->expr()->and(
-                        $queryBuilder->expr()->eq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)),
-                        $queryBuilder->expr()->eq('sys_template.uid', $queryBuilder->createNamedParameter($templateUidOnDeepestRootline, \PDO::PARAM_INT)),
-                    ),
-                ),
-            );
-        }
-        // Build a value list as joined table to have sorting based on list sorting
-        $valueList = [];
-        // @todo: Use type/int cast from expression builder to handle this dbms aware
-        //        when support for this has been extracted from CTE PoC patch (sbuerk).
-        $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform;
-        $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting';
-        foreach ($rootLinePageIds as $sorting => $rootLinePageId) {
-            $valueList[] = sprintf(
-                $pattern,
-                $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT),
-                $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT)
-            );
-        }
-        $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList);
-        $queryBuilder->getConcreteQueryBuilder()->innerJoin(
-            $queryBuilder->quoteIdentifier('sys_template'),
-            sprintf('(%s)', $valueList),
-            $queryBuilder->quoteIdentifier('pidlist'),
-            '(' . $queryBuilder->expr()->eq(
-                'sys_template.pid',
-                $queryBuilder->quoteIdentifier('pidlist.uid')
-            ) . ')'
-        );
-        // Sort by rootline determined depth as sort criteria
-        $queryBuilder->orderBy('pidlist.sorting', 'ASC')
-            ->addOrderBy('sys_template.root', 'DESC')
-            ->addOrderBy('sys_template.sorting', 'ASC');
-        $lastPid = null;
-        $queryResult = $queryBuilder->executeQuery();
-        while ($sysTemplateRow = $queryResult->fetchAssociative()) {
-            // We're retrieving *all* templates per pid, but need the first one only. The
-            // order restriction above at least takes care they're after-each-other per pid.
-            if ($lastPid === (int)$sysTemplateRow['pid']) {
-                continue;
-            }
-            $lastPid = (int)$sysTemplateRow['pid'];
-            $sysTemplateRows[] = $sysTemplateRow;
-        }
-        return $sysTemplateRows;
-    }
-
     /**
      * Get 'basedOn' sys_template sub-rows of sys_templates that use this.
      * Note the 'IN()' query implementation below delivers rows in *any* order. To preserve
@@ -731,18 +604,4 @@ final class TreeBuilder
         }
         return $restrictionContainer;
     }
-
-    /**
-     * It's ugly this class has this dependency.
-     * It is used within 'addDefaultTypoScriptConstantsFromSite()' to find the current site object.
-     *
-     * @todo: It would be better if site is either set() from outside, or the SiteFinder is used to grab
-     *        current site. But SiteFinder looks expensive to call in FE scope?! So this get()'er is a
-     *        shortcut? Note we currently *do* have the rootline available in this class ...
-     *        Call for help here @benni, old reference: TemplateService->addDefaultTypoScript()
-     */
-    private function getTypoScriptFrontendController(): ?TypoScriptFrontendController
-    {
-        return $GLOBALS['TSFE'] ?? null;
-    }
 }
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
index dc68b0d21dc09a581ad04bffb33550ee332db5b1..2ce8c8b188a1f1ca753ba9b047231b9d15116e3d 100644
--- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
@@ -35,11 +35,22 @@ final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInte
 {
     private ConditionMatcherInterface $conditionMatcher;
 
+    private array $conditionList = [];
+
     public function setConditionMatcher(ConditionMatcherInterface $conditionMatcher)
     {
         $this->conditionMatcher = $conditionMatcher;
     }
 
+    /**
+     * A list of all handled conditions with their verdicts.
+     * This is used in FE since condition verdicts influence page caches.
+     */
+    public function getConditionList(): array
+    {
+        return $this->conditionList;
+    }
+
     public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
     {
         if (!$include instanceof IncludeConditionInterface) {
@@ -48,6 +59,7 @@ final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInte
         $conditionValue = $include->getConditionToken()->getValue();
         // @todo: This bracket handling is stupid, it's removed in matcher again ...
         $verdict = $this->conditionMatcher->match('[' . $conditionValue . ']');
+        $this->conditionList[$conditionValue] = $verdict;
         $include->setConditionVerdict($verdict);
     }
 
diff --git a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
index be2092a1c94020f643cc9488f6859394d09c397a..df4a800112fd53341b94eea709cf970a6382bd68 100644
--- a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
+++ b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
@@ -30,7 +30,10 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
 
 /**
- * The TypoScript parser
+ * The TypoScript parser.
+ *
+ * @deprecated This class should not be used anymore, last core usages will be removed during v12.
+ *             Using methods or properties of this class will start logging deprecation messages.
  */
 class TypoScriptParser
 {
diff --git a/typo3/sysext/core/Classes/TypoScript/TemplateService.php b/typo3/sysext/core/Classes/TypoScript/TemplateService.php
index 7cde066a00ecb32c5e6296214a9904f276dc837f..e3aa25d60ea3a3986fbb33531d7157fb402b4020 100644
--- a/typo3/sysext/core/Classes/TypoScript/TemplateService.php
+++ b/typo3/sysext/core/Classes/TypoScript/TemplateService.php
@@ -38,8 +38,9 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * Template object that is responsible for generating the TypoScript template based on template records.
- * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
- * @see \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher
+ *
+ * @deprecated This class should not be used anymore, last core usages will be removed during v12.
+ *             Using methods or properties of this class will start logging deprecation messages.
  */
 class TemplateService
 {
@@ -363,31 +364,6 @@ class TemplateService
         }
     }
 
-    /**
-     * Fetches the "currentPageData" array from cache
-     *
-     * NOTE about currentPageData:
-     * It holds information about the TypoScript conditions along with the list
-     * of template uid's which is used on the page. In the getFromCache() function
-     * in TSFE, currentPageData is used to evaluate if there is a template and
-     * if the matching conditions are alright. Unfortunately this does not take
-     * into account if the templates in the rowSum of currentPageData has
-     * changed composition, eg. due to hidden fields or start/end time. So if a
-     * template is hidden or times out, it'll not be discovered unless the page
-     * is regenerated - at least the this->start function must be called,
-     * because this will make a new portion of data in currentPageData string.
-     *
-     * @param int $pageId
-     * @param string $mountPointValue
-     * @return array Returns the unmatched array $currentPageData if found cached in "cache_pagesection". Otherwise FALSE is returned which means that the array must be generated and stored in the cache
-     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
-     * @internal
-     */
-    public function getCurrentPageData(int $pageId, string $mountPointValue)
-    {
-        return GeneralUtility::makeInstance(CacheManager::class)->getCache('pagesection')->get($pageId . '_' . GeneralUtility::md5int($mountPointValue));
-    }
-
     /**
      * Fetches data about which TypoScript-matches there are at this page. Then it performs a matchingtest.
      *
@@ -416,7 +392,6 @@ class TemplateService
      * Sets $this->setup to the parsed TypoScript template array
      *
      * @param array $theRootLine The rootline of the current page (going ALL the way to tree root)
-     * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getConfigArray()
      */
     public function start($theRootLine)
     {
@@ -425,47 +400,18 @@ class TemplateService
             $constantsData = [];
             $setupData = [];
             $cacheIdentifier = '';
-            // Flag that indicates that the existing data in cache_pagesection
-            // could be used (this is the case if $TSFE->all is set, and the
-            // rowSum still matches). Based on this we decide if cache_pagesection
-            // needs to be updated...
-            $isCached = false;
             $this->runThroughTemplates($theRootLine);
-            if ($this->getTypoScriptFrontendController()->all) {
-                $cc = $this->getTypoScriptFrontendController()->all;
-                // The two rowSums must NOT be different from each other - which they will be if start/endtime or hidden has changed!
-                if (serialize($this->rowSum) !== serialize($cc['rowSum'])) {
-                    $cc = [];
-                } else {
-                    // If $TSFE->all contains valid data, we don't need to update cache_pagesection (because this data was fetched from there already)
-                    if (serialize($this->rootLine) === serialize($cc['rootLine'])) {
-                        $isCached = true;
-                    }
-                    // When the data is serialized below (ROWSUM hash), it must not contain the rootline by concept. So this must be removed (and added again later)...
-                    unset($cc['rootLine']);
-                }
-            }
             // This is about getting the hash string which is used to fetch the cached TypoScript template.
             // If there was some cached currentPageData ($cc) then that's good (it gives us the hash).
-            if (!empty($cc)) {
-                // If currentPageData was actually there, we match the result (if this wasn't done already in $TSFE->getFromCache()...)
-                if (!$cc['match']) {
-                    // @todo check if this can ever be the case - otherwise remove
-                    $cc = $this->matching($cc);
-                    ksort($cc);
-                }
+            // If currentPageData was not there, we first find $rowSum (freshly generated). After that we try to see, if it is stored with a list of all conditions. If so we match the result.
+            $rowSumHash = md5('ROWSUM:' . serialize($this->rowSum));
+            $result = $this->getCacheEntry($rowSumHash);
+            if (is_array($result)) {
+                $cc['all'] = $result;
+                $cc['rowSum'] = $this->rowSum;
+                $cc = $this->matching($cc);
+                ksort($cc);
                 $cacheIdentifier = md5(serialize($cc));
-            } else {
-                // If currentPageData was not there, we first find $rowSum (freshly generated). After that we try to see, if it is stored with a list of all conditions. If so we match the result.
-                $rowSumHash = md5('ROWSUM:' . serialize($this->rowSum));
-                $result = $this->getCacheEntry($rowSumHash);
-                if (is_array($result)) {
-                    $cc['all'] = $result;
-                    $cc['rowSum'] = $this->rowSum;
-                    $cc = $this->matching($cc);
-                    ksort($cc);
-                    $cacheIdentifier = md5(serialize($cc));
-                }
             }
             if ($cacheIdentifier) {
                 // Get TypoScript setup array
@@ -509,19 +455,6 @@ class TemplateService
             // Add rootLine
             $cc['rootLine'] = $this->rootLine;
             ksort($cc);
-            // Make global and save
-            $this->getTypoScriptFrontendController()->all = $cc;
-            // Matching must be executed for every request, so this must never be part of the pagesection cache!
-            unset($cc['match']);
-            if (!$isCached && !$this->simulationHiddenOrTime && !$this->getTypoScriptFrontendController()->no_cache) {
-                // Only save the data if we're not simulating by hidden/starttime/endtime
-                $mpvarHash = GeneralUtility::md5int($this->getTypoScriptFrontendController()->MP);
-                $pageSectionCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pagesection');
-                $pageSectionCache->set($this->getTypoScriptFrontendController()->id . '_' . $mpvarHash, $cc, [
-                    'pageId_' . $this->getTypoScriptFrontendController()->id,
-                    'mpvarHash_' . $mpvarHash,
-                ]);
-            }
             // If everything OK.
             if ($this->rootId && $this->rootLine && $this->setup) {
                 $this->loaded = true;
@@ -827,7 +760,7 @@ class TemplateService
      * @internal
      * @see includeStaticTypoScriptSources()
      */
-    public function addExtensionStatics($idList, $templateID, $pid)
+    protected function addExtensionStatics($idList, $templateID, $pid)
     {
         $this->extensionStaticsProcessed = true;
 
diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
index 03127c2a812090b05aa3c861ac96f854710708ce..2859a57baca043d3e62b1b268bdcca6f8c59debb 100644
--- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
+++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
@@ -1159,31 +1159,35 @@ tt_content.' . $key . $suffix . ' {
     }
 
     /**
-     * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup']
-     * Prefixed with a [GLOBAL] line
+     * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup'].
+     * NOT prefixed with a [GLOBAL] line, other calls MUST properly close their conditions!
      * FOR USE IN ext_localconf.php FILES
      *
      * @param string $content TypoScript Setup string
      */
     public static function addTypoScriptSetup(string $content): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= '
-[GLOBAL]
-' . $content;
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] ??= '';
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'])) {
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= LF;
+        }
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= $content;
     }
 
     /**
      * Adds $content to the default TypoScript constants code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_constants']
-     * Prefixed with a [GLOBAL] line
+     * NOT prefixed with a [GLOBAL] line, other calls MUST properly close their conditions!
      * FOR USE IN ext_localconf.php FILES
      *
      * @param string $content TypoScript Constants string
      */
     public static function addTypoScriptConstants(string $content): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= '
-[GLOBAL]
-' . $content;
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] ??= '';
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'])) {
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= LF;
+        }
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= $content;
     }
 
     /**
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 1c204be9035c6100a28faf2e545b43e219804f50..6db2bf6208df772ea10e88d897b2c2bee3bfb4c3 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -177,15 +177,6 @@ return [
                     ],
                     'groups' => ['pages'],
                 ],
-                'pagesection' => [
-                    'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
-                    'backend' => \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class,
-                    'options' => [
-                        'compression' => true,
-                        'defaultLifetime' => 2592000, // 30 days; set this to a lower value in case your cache gets too big
-                    ],
-                    'groups' => ['pages'],
-                ],
                 'runtime' => [
                     'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
                     'backend' => \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend::class,
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index bda6eacab24308ee563d640c3dd347dd5673babf..31be37e6be9095764f58bce6cc7295fc0cb28069 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -303,6 +303,9 @@ services:
   TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface:
     alias: TYPO3\CMS\Core\TypoScript\AST\AstBuilder
 
+  TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository:
+    public: true
+
   TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder:
     public: true
     arguments:
@@ -315,6 +318,11 @@ services:
     # Ast builder visitor creates state and should not be re-used
     shared: false
 
+  TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor:
+    public: true
+    # This visitor creates state and should not be re-used
+    shared: false
+
   TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface:
     alias: TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer
 
@@ -330,11 +338,6 @@ services:
     factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
     arguments: ['pages']
 
-  cache.pagesection:
-    class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
-    factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
-    arguments: ['pagesection']
-
   cache.runtime:
     class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
     factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4dfa6eb3d0260974c1f580f6d82c462019881978
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst
@@ -0,0 +1,77 @@
+.. include:: /Includes.rst.txt
+
+.. _breaking-97816-1664800747:
+
+====================================================
+Breaking: #97816 - New TypoScript parser in Frontend
+====================================================
+
+See :issue:`97816`
+
+Description
+===========
+
+The rewrite of the TypoScript parser has been enabled for Frontend
+rendering.
+
+See :ref:`breaking-97816-1656350406` and :ref:`feature-97816-1656350667`
+for more details on the new parser.
+
+
+Impact
+======
+
+The change has impact on Frontend caching, hooks, some classes and properties. In detail:
+
+* Hook :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing']`
+  is gone and substituted by :php:`AfterTemplatesHaveBeenDeterminedEvent`. See :ref:`feature-97816-1664801053` for more details.
+
+* The classes :php:`TYPO3\CMS\Core\TypoScript\TemplateService` and :php:`TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser`
+  have been marked as deprecated and shouldn't be used anymore.
+  An instance of :php:`TemplateService` is still kept as property :php:`TypoScriptFrontendController->tmpl` (:php:`$GLOBALS['TSFE']->tmpl)
+  as backwards compatible layer, and the most important properties within the class, namely especially :php:`TemplateService->setup` is
+  still set. To avoid using these properties, the Frontend request object will contain this state.
+  In rare cases, where extensions need to parse TypoScript on their own, they should switch to the Tokenizer and AstBuilder structures
+  of the new parser. Note these classes are still young and currently marked @internal, the API may still slightly change with further
+  v12 development.
+
+* The :php:`pagesection` cache has been removed. This was a helper cache that grew O(n) with the number of
+  called Frontend pages. The new :php:`typoscript` cache is used instead: This grows only O(n) with the
+  number of different sys_template and condition combinations and is a filesystem based :php:`PhpFrontend` implementation.
+  When upgrading, the database tables :sql:`cache_pagesection` and :sql:`cache_pagesections_tags` can be safely removed, the
+  install tool will also silently remove any existing entries from :file:`settings.php` that reconfigure the cache.
+
+* The following properties and methods in :php:`TypoScriptFrontendController` have been set to :php:`@internal` and should not
+  be used any longer since they may vanish without further notice:
+
+  * :php:`TypoScriptFrontendController->no_cache`
+  * :php:`TypoScriptFrontendController->tmpl`
+  * :php:`TypoScriptFrontendController->pageContentWasLoadedFromCache`
+  * :php:`TypoScriptFrontendController->getFromCache_queryRow()`
+  * :php:`TypoScriptFrontendController->populatePageDataFromCache()`
+  * :php:`TypoScriptFrontendController->shouldAcquireCacheData()`
+  * :php:`TypoScriptFrontendController->acquireLock()`
+  * :php:`TypoScriptFrontendController->releaseLock()`
+
+* The following methods in :php:`TypoScriptFrontendController` have been removed:
+
+  * :php:`TypoScriptFrontendController->getHash()`
+  * :php:`TypoScriptFrontendController->getLockHash()`
+  * :php:`TypoScriptFrontendController->getConfigArray()`
+  * :php:`TypoScriptFrontendController->()`
+
+
+Affected installations
+======================
+
+Many instances will only recognize that the :php:`pagesection` cache is gone and should continue to work.
+Instances with extensions that use :php:`TemplateService` or :php:`TypoScriptParser`, or access the
+property :php:`TypoScriptFrontendController->tmpl` may need adaptions.
+
+
+Migration
+=========
+
+See the impact description above for some migration hints.
+
+.. index:: Database, Frontend, PHP-API, TypoScript, LocalConfiguration, PartiallyScanned, ext:frontend
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f501ead1c3ae51ecee9cb428a98746a9ef58b77c
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst
@@ -0,0 +1,33 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-97816-1664801053:
+
+===========================================================
+Feature: #97816 - New AfterTemplatesHaveBeenDeterminedEvent
+===========================================================
+
+See :issue:`97816`
+
+Description
+===========
+
+With switching to the new TypoScript parser, hook
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing']`
+has been removed.
+
+The new event :php:`AfterTemplatesHaveBeenDeterminedEvent` can be used
+to manipulate sys_template rows. The event receives the list of resolved
+sys_template rows and the :php:`SiteInterface` and allows manipulating the
+sys_template rows array.
+
+
+Impact
+======
+
+The event is called in Backend EXT:tstemplate code, for example in the Template Analyzer,
+and - more importantly - in the Frontend.
+
+Extensions using the old hook that want to stay compatible with both core v11 and v12
+can implement both.
+
+.. index:: PHP-API, TypoScript, ext:core
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..11d98065aa131834b5c2ae4bd08ce8a9384c6abd
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php
@@ -0,0 +1,95 @@
+<?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\Core\Tests\Functional\TypoScript\IncludeTree;
+
+use TYPO3\CMS\Core\Site\Entity\NullSite;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class SysTemplateRepositoryTest extends FunctionalTestCase
+{
+    /**
+     * @test
+     */
+    public function singleRootTemplate(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/singleRootTemplate.csv');
+        $rootline = [
+            [
+                'uid' => 1,
+                'pid' => 0,
+                'is_siteroot' => 0,
+            ],
+        ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite());
+        self::assertSame(1, $result[0]['uid']);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 1);
+        self::assertSame(1, $result[0]['uid']);
+    }
+
+    /**
+     * @test
+     */
+    public function twoPagesTwoTemplates(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/twoPagesTwoTemplates.csv');
+        $rootline = [
+            [
+                'uid' => 2,
+                'pid' => 1,
+                'is_siteroot' => 0,
+            ],
+            [
+                'uid' => 1,
+                'pid' => 0,
+                'is_siteroot' => 0,
+            ],
+        ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite());
+        self::assertSame(1, $result[0]['uid']);
+        self::assertSame(2, $result[1]['uid']);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 2);
+        self::assertSame(1, $result[0]['uid']);
+        self::assertSame(2, $result[1]['uid']);
+    }
+
+    /**
+     * @test
+     */
+    public function twoTemplatesOnPagePrefersTheOneWithLowerSorting(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/twoTemplatesOnPage.csv');
+        $rootline = [
+            [
+                'uid' => 1,
+                'pid' => 0,
+                'is_siteroot' => 0,
+            ],
+        ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite());
+        self::assertSame(1, $result[0]['uid']);
+        $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 2);
+        self::assertSame(2, $result[0]['uid']);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php
index cd72c116a3a27b1b2384c91daf9112c448acae31..0c5212b092acf0eb793c2a896f9055c8554b5f4e 100644
--- a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php
@@ -18,9 +18,12 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Functional\TypoScript\IncludeTree;
 
 use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Site\Entity\NullSite;
+use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
 use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
@@ -72,9 +75,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
     }
@@ -93,9 +98,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertSame('barValue', $ast->getChildByName('bar')->getValue());
@@ -114,9 +121,17 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
+        /** @var SiteFinder $siteFinder */
+        $siteFinder = $this->get(SiteFinder::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite(
+            'constants',
+            $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()),
+            $siteFinder->getSiteByPageId(1)
+        );
         $ast = $this->getAst($includeTree);
         self::assertSame('testValueFromSite', $ast->getChildByName('testConstantFromSite')->getValue());
     }
@@ -139,9 +154,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertSame('barValue', $ast->getChildByName('bar')->getValue());
@@ -165,9 +182,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertNull($ast->getChildByName('foo'));
         self::assertSame('barValue', $ast->getChildByName('bar')->getValue());
@@ -186,9 +205,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertNull($ast->getChildByName('bar'));
@@ -207,9 +228,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertSame('loadedByBasedOn', $ast->getChildByName('bar')->getValue());
@@ -228,9 +251,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertSame('loadedByBasedOn', $ast->getChildByName('bar')->getValue());
@@ -249,9 +274,11 @@ class TreeBuilderTest extends FunctionalTestCase
                 'is_siteroot' => 0,
             ],
         ];
+        /** @var SysTemplateRepository $sysTemplateRepository */
+        $sysTemplateRepository = $this->get(SysTemplateRepository::class);
         /** @var TreeBuilder $treeBuilder */
         $treeBuilder = $this->get(TreeBuilder::class);
-        $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false);
+        $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()));
         $ast = $this->getAst($includeTree);
         self::assertSame('fooValue', $ast->getChildByName('foo')->getValue());
         self::assertSame('includeStaticTarget', $ast->getChildByName('bar')->getValue());
diff --git a/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php b/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php
index 66e2a2c8a1fff023c58cc728a00097afe1a957e7..bedba3c82b4b55b79098bebace60e2260c16c804 100644
--- a/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php
+++ b/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php
@@ -64,10 +64,6 @@ abstract class AbstractRequestHandlingTest extends FunctionalTestCase
                         'backend' => Typo3DatabaseBackend::class,
                         'frontend' => VariableFrontend::class,
                     ],
-                    'pagesection' => [
-                        'backend' => Typo3DatabaseBackend::class,
-                        'frontend' => VariableFrontend::class,
-                    ],
                     'rootline' => [
                         'backend' => Typo3DatabaseBackend::class,
                         'frontend' => VariableFrontend::class,
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index a9a9967bb0d9a6d0414ea1f26cb0de4e5f47e02e..367a71b412193efaa0104e8bc829a22d8d85565e 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -57,6 +57,13 @@ 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\TypoScript\AST\Node\ChildNode;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
@@ -66,7 +73,7 @@ use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
-use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
+use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;
 use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent;
@@ -129,6 +136,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * Page will not be cached. Write only TRUE. Never clear value (some other
      * code might have reasons to set it TRUE).
      * @var bool
+     * @internal
      */
     public $no_cache = false;
 
@@ -251,6 +259,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * The TypoScript template object. Used to parse the TypoScript template
      *
      * @var TemplateService
+     * @internal: Will get a proper deprecation in v12.x.
+     * @deprecated: TemplateService is kept for b/w compat in v12 but will be removed in v13.
      */
     public $tmpl;
 
@@ -264,6 +274,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
 
     /**
      * Set if cached content was fetched from the cache.
+     * @internal
      */
     protected bool $pageContentWasLoadedFromCache = false;
 
@@ -274,24 +285,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     protected int $cacheExpires = 0;
 
     /**
-     * Used by template fetching system. This array is an identification of
-     * the template. If $this->all is empty it's because the template-data is not
-     * cached, which it must be.
-     * @var array
-     * @internal
-     */
-    public $all = [];
-
-    /**
-     * Toplevel - objArrayName, eg 'page'
-     * @var string
-     * @internal should only be used by TYPO3 Core
-     */
-    public $sPre = '';
-
-    /**
-     * TypoScript configuration of the page-object pointed to by sPre.
-     * $this->tmpl->setup[$this->sPre.'.']
+     * TypoScript configuration of the page-object.
      * @var array|string
      * @internal should only be used by TYPO3 Core
      */
@@ -575,7 +569,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     protected function initCaches()
     {
-        $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
+        $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
+        $this->pageCache = $cacheManager->getCache('pages');
     }
 
     /**
@@ -1119,103 +1114,401 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
-     * See if page is in cache and get it if so, populates the page content to $this->content.
-     * Also fetches the raw cached pagesection information (TypoScript information) before.
+     * This is a central and quite early method called by PrepareTypoScriptFrontendRendering middleware:
+     * This code is *always* executed for *every* frontend call if a general page rendering has to be done,
+     * if there is no early redirect or eid call or similar.
      *
-     * @param ServerRequestInterface $request
+     * The goal is to calculate dependencies up to a point to see if a possible page cache can be used,
+     * and to prepare TypoScript as far as really needed.
+     *
+     * @throws PropagateResponseException
+     * @throws AbstractServerErrorException
+     *
+     * @internal This method may vanish from TypoScriptFrontendController without further notice.
+     * @todo: This method is typically called by PrepareTypoScriptFrontendRendering middleware.
+     *        However, the RedirectService of (earlier) ext:redirects RedirectHandler middleware
+     *        calls this as well. We may want to put this code into some helper class, reduce class
+     *        state as much as possible and carry really needed state as request attributes around?!
      */
-    public function getFromCache(ServerRequestInterface $request)
+    public function getFromCache(ServerRequestInterface $request): void
     {
-        // clearing the content-variable, which will hold the pagecontent
+        // Reset some state.
+        // @todo: Find out which resets are really needed here - Since this is called from a
+        //        relatively early middleware, we can expect these properties to be not set already?!
         $this->content = '';
-        // Unsetting the lowlevel config
         $this->config = [];
         $this->pageContentWasLoadedFromCache = false;
 
-        if ($this->no_cache) {
-            return;
+        // Very first thing, *always* executed: TypoScript is one factor that influences page content.
+        // There can be multiple cache entries per page, when TypoScript conditions on the same page
+        // create different TypoScript. We thus need the sys_template rows relevant for this page.
+        // @todo: Even though all rootline sys_template records are fetched with only one query
+        //        in below implementation, we could potentially join or sub select sys_template
+        //        records already when pages rootline is queried. This will safe one query
+        //        and needs an implementation in getPageAndRootline() which is called via determineId()
+        //        in TypoScriptFrontendInitialization. This could be done when getPageAndRootline()
+        //        switches to a CTE query instead of using RootlineUtility.
+        $sysTemplateRepository = GeneralUtility::makeInstance(SysTemplateRepository::class);
+        $sysTemplateRows = $sysTemplateRepository->getSysTemplateRowsByRootline($this->rootLine, $this->getSite());
+
+        // Early exception if there is no sys_template at all.
+        if (empty($sysTemplateRows)) {
+            $message = 'No TypoScript template found!';
+            $this->logger->alert($message);
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
+                    $request,
+                    $message,
+                    ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
+                );
+                throw new PropagateResponseException($response, 1533931380);
+            } catch (AbstractServerErrorException $e) {
+                $exceptionClass = get_class($e);
+                throw new $exceptionClass($message, 1294587218);
+            }
         }
 
         if (!$this->tmpl instanceof TemplateService) {
+            // @todo: Well, TemplateService should be deprecated entirely, soon. We're essentially not
+            //        using the old parser anymore and some properties are later just set for b/w compat
+            //        reasons. The if() probably only exists to allow early setting of this property to
+            //        something in tests, other than that, we could expect it to be null at
+            //        this point in the middleware stack.
             $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
         }
 
-        $pageSectionCacheContent = $this->tmpl->getCurrentPageData($this->id, (string)$this->MP);
-        if (!is_array($pageSectionCacheContent)) {
-            // Nothing in the cache, we acquire an "exclusive lock" for the key now.
-            // We use the Registry to store this lock centrally,
-            // but we protect the access again with a global exclusive lock to avoid race conditions
-
-            $this->acquireLock('pagesection', $this->id . '::' . $this->MP);
-            //
-            // from this point on we're the only one working on that page ($key)
-            //
-
-            // query the cache again to see if the page data are there meanwhile
-            $pageSectionCacheContent = $this->tmpl->getCurrentPageData($this->id, (string)$this->MP);
-            if (is_array($pageSectionCacheContent)) {
-                // we have the content, nice that some other process did the work for us already
-                $this->releaseLock('pagesection');
-            }
-            // We keep the lock set, because we are the ones generating the page now and filling the cache.
-            // This indicates that we have to release the lock later in releaseLocks()
-        }
-
-        if (is_array($pageSectionCacheContent)) {
-            // BE CAREFUL to change the content of the cc-array. This array is serialized and an md5-hash based on this is used for caching the page.
-            // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
-            // This array is an identification of the template. If $this->all is empty it's because the template-data is not cached, which it must be.
-            $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
-            ksort($pageSectionCacheContent);
-            $this->all = $pageSectionCacheContent;
+        if ($this->no_cache) {
+            // $this->no_cache = true might have been set by earlier TypoScriptFrontendInitialization middleware.
+            // This means we don't do any fancy cache stuff, calculate full TypoScript and ignore page cache.
+            $this->prepareUncachedRendering($request, $sysTemplateRows);
+            return;
         }
 
-        // Look for page in cache only if a shift-reload is not sent to the server.
-        $lockHash = $this->getLockHash();
-        if ($this->shouldAcquireCacheData($request) && $this->all) {
-            // we got page section information (TypoScript), so lets see if there is also a cached version
-            // of this page in the pages cache.
-            $this->newHash = $this->getHash();
+        // We *always* need the TypoScript constants, one way or the other: Setup conditions can use constants,
+        // so we need the constants to substitute their values within setup conditions.
+        // @todo: This is currently a rather naive approach - we simply always create the full constant AST plus
+        //        $flatConstants. This shouldn't be *that* bad since the include tree is already fetched from cache,
+        //        so "only" the AST parsing is done, and constants are typically at least an order of magnitude smaller
+        //        than setup and thus relatively quick to parse.
+        //        There are however further optimization opportunities: The constant AST only depends on sys_template
+        //        rows (which have to be *always* queried anyways), plus condition verdicts, plus maybe SIM_ACCESS_TIME?!
+        //        We could thus fetch only the IncludeTree and have a traverser that calculates and gathers conditions verdicts.
+        //        Then hash $sysTemplateRows together with conditions list and it's verdicts, and use that as cache identifier.
+        //        We could then cache the constant AST, flat constants and maybe even the AST as array in one cache entry
+        //        with this identifier. Potential improvement: A relatively small cache entry for the constants, no AST
+        //        parsing, re-usable for many pages that have the same sys_template combination and condition verdicts.
+        //        So probably not too many cache entries, either. This may squeeze some time out of this construct and is
+        //        beneficial since the code below is *always* executed even in full cached context.
+        $treeBuilder = GeneralUtility::makeInstance(TreeBuilder::class);
+        $includeTreeTraverser = GeneralUtility::makeInstance(ConditionVerdictAwareIncludeTreeTraverser::class);
+        $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->getSite());
+        $includeTreeTraverser->resetVisitors();
+        $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
+        $conditionMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine));
+        $includeTreeTraverser->addVisitor($conditionMatcherVisitor);
+        $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
+        $includeTreeTraverser->addVisitor($constantAstBuilderVisitor);
+        $includeTreeTraverser->traverse($constantIncludeTree);
+        $constantAst = $constantAstBuilderVisitor->getAst();
+        $constantConditionList = $conditionMatcherVisitor->getConditionList();
+        $flatConstants = $constantAst->flatten();
+
+        // Next step: We have constants and fetch the setup include tree now. We then calculate setup condition verdicts
+        // and set the constants to allow substitution of constants within conditions. We then traverse the include tree
+        // to calculate conditions verdicts and gather them along the way. A hash of these conditions with their verdicts
+        // is then part of the page cache identifier hash: When a condition on a page creates a different result, the hash
+        // is different from an existing page cache entry and a new one is created later.
+        // @todo: There is quite some optimization potential here: First, we don't need the tokenized TS streams from the
+        //        IncludeTree cache entries here. We could thus create an IncludeTree cache entry that does not have
+        //        the token streams. This will create a significantly smaller cache entry that is quicker to require and has
+        //        a better chance to end up in opcache, too. We could also cache the final AST and maybe the array representation
+        //        with an identifier created from the constant hash plus the condition hash. This would allow us to not parse AST
+        //        at all, but just require the final AST in many cases directly. Both above strategies would be caches shared and
+        //        re-usable between multiple different pages when constants and conditions resolve identical, which will safe a ton
+        //        of time in both not-yet-cached-page and user-int scenarios since AST parsing is gone.
+        $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->getSite());
+        $includeTreeTraverser->resetVisitors();
+        $setupConditionConstantSubstitutionVisitor = GeneralUtility::makeInstance(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
+        $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
+        $includeTreeTraverser->addVisitor($setupConditionConstantSubstitutionVisitor);
+        // @todo: Maybe split IncludeTreeConditionMatcherVisitor into two implementations? One that gathers conditions and one that does not?
+        $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
+        $setupMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine));
+        $includeTreeTraverser->addVisitor($setupMatcherVisitor);
+        $includeTreeTraverser->traverse($setupIncludeTree);
+        $setupConditionList = $setupMatcherVisitor->getConditionList();
+
+        $lockHash = $this->createLockHash();
+        $this->newHash = $this->id . '_' . md5($this->createHashBase($sysTemplateRows, $constantConditionList, $setupConditionList));
+        if ($this->shouldAcquireCacheData($request)) {
+            // Try to get a page cache row.
             $this->getTimeTracker()->push('Cache Row');
-            $row = $this->getFromCache_queryRow();
-            if (!is_array($row)) {
-                // nothing in the cache, we acquire an exclusive lock now
+            $pageCacheRow = $this->getFromCache_queryRow();
+            if (!is_array($pageCacheRow)) {
+                // Nothing in the cache, we acquire an exclusive lock now.
+                // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll
+                // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In
+                // this case, the below call will wait until the lock is released again. The other process then probably wrote
+                // a page cache entry, which we can use.
+                // To handle the second case - if our process had to wait for another one creating the content for us - we
+                // simply query the page cache again to see if there is a page cache now. This has the drawback that the page
+                // cache is queried twice if the lock did not had to wait for another process ... Maybe we could suppress this?
                 $this->acquireLock('pages', $lockHash);
-                //
-                // from this point on we're the only one working on that page ($lockHash)
-                //
-
-                // query the cache again to see if the data are there meanwhile
-                $row = $this->getFromCache_queryRow();
-                if (is_array($row)) {
-                    // we have the content, nice that some other process did the work for us
+                // From this point on we're the only one working on that page ($lockHash).
+                // Query the cache again to see if the data is there meanwhile.
+                $pageCacheRow = $this->getFromCache_queryRow();
+                if (is_array($pageCacheRow)) {
+                    // We have the content, some other process did the work for us, release our lock again.
                     $this->releaseLock('pages');
                 }
                 // We keep the lock set, because we are the ones generating the page now and filling the cache.
-                // This indicates that we have to release the lock later in releaseLocks()
+                // This indicates that we have to release the lock later in releaseLocks()!
             }
-            if (is_array($row)) {
-                $this->populatePageDataFromCache($row);
+            if (is_array($pageCacheRow)) {
+                // Note this especially populates $this->config!
+                $this->populatePageDataFromCache($pageCacheRow);
             }
             $this->getTimeTracker()->pull();
         } else {
-            // the user forced rebuilding the page cache or there was no pagesection information
-            // get a lock for the page content so other processes will not interrupt the regeneration
+            // User forced page cache rebuilding. Get a lock for the page content so other processes can't interfere.
             $this->acquireLock('pages', $lockHash);
         }
+
+        if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
+            // We don't have a cache entry for this page, or the user force TS parsing (do we really need this feature?), or
+            // (more importantly), the cache page row contains "INT" objects. In these cases, we have to boot up the rest of
+            // TypoScript: We need the full AST / array!
+            // Since we get the IncludeTree above already, and calculated condition verdicts (that's not very expensive anyways),
+            // We just need to reset $includeTreeTraverser visitors and only add the AST builder visitor to create the final AST.
+            // @todo: This code part should change as soon as the above mentioned "more effective setup AST building"
+            //        strategy is implemented: We could still fetch the final AST from cache without parsing here.
+            $includeTreeTraverser->resetVisitors();
+            $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
+            $setupAstBuilderVisitor->setFlatConstants($flatConstants);
+            $includeTreeTraverser->addVisitor($setupAstBuilderVisitor);
+            $includeTreeTraverser->traverse($setupIncludeTree);
+            $setupAst = $setupAstBuilderVisitor->getAst();
+
+            // Create top-level setup AST 'types' node from all top-level PAGE objects.
+            // This is essentially a preparation for type-lookup below and should vanish later.
+            // Previously, TemplateService->generateConfig() did that.
+            $typesNode = new ChildNode('types');
+            $gotTypeNumZero = false;
+            foreach ($setupAst->getNextChild() as $setupChild) {
+                if ($setupChild->getValue() === 'PAGE') {
+                    $typeNumChild = $setupChild->getChildByName('typeNum');
+                    if ($typeNumChild) {
+                        $typeNumValue = $typeNumChild->getValue();
+                        $typesSubNode = new ChildNode($typeNumValue);
+                        $typesSubNode->setValue($setupChild->getName());
+                        $typesNode->addChild($typesSubNode);
+                        if ($typeNumValue === '0') {
+                            $gotTypeNumZero = true;
+                        }
+                    } elseif (!$gotTypeNumZero) {
+                        // The first PAGE node that has no typeNum = 0 is considered
+                        // '0' automatically.
+                        $typesSubNode = new ChildNode('0');
+                        $typesSubNode->setValue($setupChild->getName());
+                        $typesNode->addChild($typesSubNode);
+                        $gotTypeNumZero = true;
+                    }
+                }
+            }
+            if ($typesNode->hasChildren()) {
+                $setupAst->addChild($typesNode);
+            }
+
+            $setupAstArray = $setupAst->toArray();
+
+            $typoScriptPageTypeName = $setupAstArray['types.'][$this->type] ?? '';
+            $this->pSetup = $setupAstArray[$typoScriptPageTypeName . '.'] ?? '';
+
+            if (!is_array($this->pSetup)) {
+                $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]);
+                try {
+                    $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
+                        $request,
+                        $message,
+                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
+                    );
+                    throw new PropagateResponseException($response, 1533931374);
+                } catch (AbstractServerErrorException $e) {
+                    $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
+                    $exceptionClass = get_class($e);
+                    throw new $exceptionClass($message . ' ' . $explanation, 1294587217);
+                }
+            }
+
+            if (!isset($this->config['config'])) {
+                $this->config['config'] = [];
+            }
+            // Filling the config-array, first with the main "config." part
+            if (is_array($setupAstArray['config.'] ?? null)) {
+                $setupAstArray['config.'] = array_replace_recursive($setupAstArray['config.'], $this->config['config']);
+                $this->config['config'] = $setupAstArray['config.'];
+            }
+            // override it with the page/type-specific "config."
+            if (is_array($this->pSetup['config.'] ?? null)) {
+                $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']);
+            }
+            // Processing for the config_array:
+            $this->config['rootLine'] = array_reverse($this->rootLine);
+
+            // b/w compat, especially for bootstrap_package which does a lot of magic with constants
+            $this->tmpl->setup = $setupAstArray;
+            $this->tmpl->loaded = true;
+            $this->tmpl->rootLine = array_reverse($this->rootLine);
+            $this->tmpl->flatSetup = $flatConstants;
+        }
+
+        // @todo: To phase out TemplateService, we should attach $constantAst and $setupAst (and maybe their array
+        //        representations as well for a transition phase) as attributes to the request here and also below
+        //        in prepareUncachedRendering().
+
+        // Set $this->no_cache TRUE if the config.no_cache value is set!
+        if ($this->config['config']['no_cache'] ?? false) {
+            $this->set_no_cache('config.no_cache is set', true);
+        }
+
+        // Auto-configure settings when a site is configured
+        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
+
+        // Hook for postProcessing the configuration array
+        $params = ['config' => &$this->config['config']];
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
+            GeneralUtility::callUserFunction($funcRef, $params, $this);
+        }
+    }
+
+    /**
+     * An early method branching within getFromCache() if caching has been disabled.
+     * @todo: This is currently not cleaned up well and duplicates code from getFromCache().
+     *        This should be cleaned up and merged with getFromCache() in a more efficient way, for
+     *        now it is a simple solution to keep the scenarios apart.
+     */
+    private function prepareUncachedRendering(ServerRequestInterface $request, array $sysTemplateRows): void
+    {
+        $includeTreeTraverser = GeneralUtility::makeInstance(ConditionVerdictAwareIncludeTreeTraverser::class);
+        $treeBuilder = GeneralUtility::makeInstance(TreeBuilder::class);
+        $treeBuilder->disableCache();
+
+        $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->getSite());
+        $includeTreeTraverser->resetVisitors();
+        $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
+        $conditionMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine));
+        $includeTreeTraverser->addVisitor($conditionMatcherVisitor);
+        $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
+        $includeTreeTraverser->addVisitor($constantAstBuilderVisitor);
+        $includeTreeTraverser->traverse($constantIncludeTree);
+
+        $constantAst = $constantAstBuilderVisitor->getAst();
+        $flatConstants = $constantAst->flatten();
+
+        $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->getSite());
+        $includeTreeTraverser->resetVisitors();
+        $setupConditionConstantSubstitutionVisitor = GeneralUtility::makeInstance(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
+        $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
+        $includeTreeTraverser->addVisitor($setupConditionConstantSubstitutionVisitor);
+        $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
+        $setupMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine));
+        $includeTreeTraverser->addVisitor($setupMatcherVisitor);
+        $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
+        $setupAstBuilderVisitor->setFlatConstants($flatConstants);
+        $includeTreeTraverser->addVisitor($setupAstBuilderVisitor);
+        $includeTreeTraverser->traverse($setupIncludeTree);
+        $setupAst = $setupAstBuilderVisitor->getAst();
+
+        // Create top-level setup AST 'types' node from all top-level PAGE objects.
+        // This is essentially a preparation for type-lookup below and should vanish later.
+        // Previously, TemplateService->generateConfig() did this.
+        $typesNode = new ChildNode('types');
+        $gotTypeNumZero = false;
+        foreach ($setupAst->getNextChild() as $setupChild) {
+            if ($setupChild->getValue() === 'PAGE') {
+                $typeNumChild = $setupChild->getChildByName('typeNum');
+                if ($typeNumChild) {
+                    $typeNumValue = $typeNumChild->getValue();
+                    $typesSubNode = new ChildNode($typeNumValue);
+                    $typesSubNode->setValue($setupChild->getName());
+                    $typesNode->addChild($typesSubNode);
+                    if ($typeNumValue === '0') {
+                        $gotTypeNumZero = true;
+                    }
+                } elseif (!$gotTypeNumZero) {
+                    // The first PAGE node that has no typeNum = 0 is considered '0' automatically.
+                    $typesSubNode = new ChildNode('0');
+                    $typesSubNode->setValue($setupChild->getName());
+                    $typesNode->addChild($typesSubNode);
+                    $gotTypeNumZero = true;
+                }
+            }
+        }
+        if ($typesNode->hasChildren()) {
+            $setupAst->addChild($typesNode);
+        }
+
+        $setupAstArray = $setupAst->toArray();
+
+        $typoScriptPageTypeName = $setupAstArray['types.'][$this->type] ?? '';
+        $this->pSetup = $setupAstArray[$typoScriptPageTypeName . '.'] ?? '';
+
+        if (!is_array($this->pSetup)) {
+            $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]);
+            try {
+                $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
+                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
+                    $request,
+                    $message,
+                    ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
+                );
+                // @todo: use the timestamp from above same call when extracting this to a method.
+                throw new PropagateResponseException($response, 1664672015);
+            } catch (AbstractServerErrorException $e) {
+                $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
+                $exceptionClass = get_class($e);
+                throw new $exceptionClass($message . ' ' . $explanation, 1664672036);
+            }
+        }
+
+        $this->config['config'] = [];
+        // Filling the config-array, first with the main "config." part
+        if (is_array($setupAstArray['config.'] ?? null)) {
+            $setupAstArray['config.'] = array_replace_recursive($setupAstArray['config.'], $this->config['config']);
+            $this->config['config'] = $setupAstArray['config.'];
+        }
+        // override it with the page/type-specific "config."
+        if (is_array($this->pSetup['config.'] ?? null)) {
+            $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']);
+        }
+        // Processing for the config_array:
+        $this->config['rootLine'] = array_reverse($this->rootLine);
+        // Auto-configure settings when a site is configured
+        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
+
+        // b/w compat, especially for bootstrap_package which does a lot of magic with constants
+        $this->tmpl->setup = $setupAstArray;
+        $this->tmpl->loaded = true;
+        $this->tmpl->rootLine = array_reverse($this->rootLine);
+        $this->tmpl->flatSetup = $flatConstants;
+
+        // Hook for postProcessing the configuration array
+        $params = ['config' => &$this->config['config']];
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
+            GeneralUtility::callUserFunction($funcRef, $params, $this);
+        }
     }
 
     /**
      * Returning the cached version of page with hash = newHash
      *
      * @return array Cached row, if any. Otherwise void.
+     * @internal
      */
     public function getFromCache_queryRow()
     {
-        $this->getTimeTracker()->push('Cache Query');
-        $row = $this->pageCache->get($this->newHash);
-        $this->getTimeTracker()->pull();
-        return $row;
+        return $this->pageCache->get($this->newHash);
     }
 
     /**
@@ -1227,6 +1520,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @see getFromCache()
      * @see setPageCacheContent()
      * @param array $cachedData
+     * @internal
      */
     protected function populatePageDataFromCache(array $cachedData): void
     {
@@ -1268,6 +1562,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
      *
      * @return bool If shift-reload in client browser has been clicked, disable getting cached page and regenerate the page content.
+     * @internal
      */
     protected function shouldAcquireCacheData(ServerRequestInterface $request): bool
     {
@@ -1285,31 +1580,43 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
-     * Calculates the cache-hash
-     * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array
-     * Used to get and later store the cached data.
-     *
-     * @return string MD5 hash of serialized hash base from createHashBase(), prefixed with page id
-     * @see getFromCache()
-     * @see getLockHash()
-     */
-    protected function getHash(): string
-    {
-        return $this->id . '_' . md5($this->createHashBase(false));
-    }
-
-    /**
-     * Calculates the lock-hash
-     * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
-     *
-     * @return string MD5 hash prefixed with page id
-     * @see getFromCache()
-     * @see getHash()
+     * This creates a hash used to lock generation for a specific page: When multiple requests try
+     * to render the same page, this lock allows creating by one request which typically puts the result
+     * into page cache, while the other requests wait until this finished and re-use the result.
+     * This method is similar to createHashBase() which is used as identifier for the page cache entry,
+     * with the exception that it does not include TypoScript constant and constant / setup condition
+     * results. This means multiple "different" calls to the same page still block each other when their
+     * TypoScript conditions verdicts are different.
+     * @todo: Find out if we couldn't simply use createHashBase() instead. Why should different calls to
+     *        the same page that will lead to different page cache entries block each other?
      */
-    protected function getLockHash(): string
+    private function createLockHash(): string
     {
-        $lockHash = $this->createHashBase(true);
-        return $this->id . '_' . md5($lockHash);
+        /** @var UserAspect $userAspect */
+        $userAspect = $this->context->getAspect('frontend.user');
+        $hashParameters = [
+            'id' => $this->id,
+            'type' => $this->type,
+            'groupIds' => (string)implode(',', $userAspect->getGroupIds()),
+            'MP' => (string)$this->MP,
+            'site' => $this->site->getIdentifier(),
+            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
+            // is not cached properly as we don't have any language-specific conditions anymore
+            'siteBase' => (string)$this->language->getBase(),
+            // additional variation trigger for static routes
+            'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
+            // dynamic route arguments (if route was resolved)
+            'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
+        ];
+        // Call hook to influence the hash calculation
+        $_params = [
+            'hashParameters' => &$hashParameters,
+            'createLockHashBase' => true,
+        ];
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
+            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
+        }
+        return $this->id . '_' . md5(serialize($hashParameters));
     }
 
     /**
@@ -1319,10 +1626,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * ->MP (Mount Points) and cHash array
      * Used to get and later store the cached data.
      *
-     * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information)
      * @return string the serialized hash base
      */
-    protected function createHashBase($createLockHashBase = false)
+    protected function createHashBase(array $sysTemplateRows, array $constantConditionList, array $setupConditionList): string
     {
         // Fetch the list of user groups
         /** @var UserAspect $userAspect */
@@ -1340,15 +1646,14 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
             // dynamic route arguments (if route was resolved)
             'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
+            'sysTemplateRows' => $sysTemplateRows,
+            'constantConditionList' => $constantConditionList,
+            'setupConditionList' => $setupConditionList,
         ];
-        // Include the template information if we shouldn't create a lock hash
-        if (!$createLockHashBase) {
-            $hashParameters['all'] = $this->all;
-        }
         // Call hook to influence the hash calculation
         $_params = [
             'hashParameters' => &$hashParameters,
-            'createLockHashBase' => $createLockHashBase,
+            'createLockHashBase' => false,
         ];
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
             GeneralUtility::callUserFunction($_funcRef, $_params, $this);
@@ -1356,100 +1661,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         return serialize($hashParameters);
     }
 
-    /**
-     * Checks if config-array exists already but if not, gets it
-     *
-     * @param ServerRequestInterface $request
-     * @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException
-     * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException
-     */
-    public function getConfigArray(ServerRequestInterface $request): void
-    {
-        if (!$this->tmpl instanceof TemplateService) {
-            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
-        }
-
-        // If config is not set by the cache (which would be a major mistake somewhere) OR if INTincScripts-include-scripts have been registered, then we must parse the template in order to get it
-        if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
-            $timeTracker = $this->getTimeTracker();
-            $timeTracker->push('Parse template');
-            // Start parsing the TS template. Might return cached version.
-            $this->tmpl->start($this->rootLine);
-            $timeTracker->pull();
-            // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()),
-            // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache())
-            $this->releaseLock('pagesection');
-            if ($this->tmpl->loaded) {
-                $timeTracker->push('Setting the config-array');
-                // toplevel - objArrayName
-                $typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type] ?? '';
-                $this->sPre = $typoScriptPageTypeName;
-                $this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.'] ?? '';
-                if (!is_array($this->pSetup)) {
-                    $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]);
-                    try {
-                        $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
-                        $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
-                            $request,
-                            $message,
-                            ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
-                        );
-                        throw new PropagateResponseException($response, 1533931374);
-                    } catch (AbstractServerErrorException $e) {
-                        $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
-                        $exceptionClass = get_class($e);
-                        throw new $exceptionClass($message . ' ' . $explanation, 1294587217);
-                    }
-                } else {
-                    if (!isset($this->config['config'])) {
-                        $this->config['config'] = [];
-                    }
-                    // Filling the config-array, first with the main "config." part
-                    if (is_array($this->tmpl->setup['config.'] ?? null)) {
-                        $this->tmpl->setup['config.'] = array_replace_recursive($this->tmpl->setup['config.'], $this->config['config']);
-                        $this->config['config'] = $this->tmpl->setup['config.'];
-                    }
-                    // override it with the page/type-specific "config."
-                    if (is_array($this->pSetup['config.'] ?? null)) {
-                        $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']);
-                    }
-                    // Processing for the config_array:
-                    $this->config['rootLine'] = $this->tmpl->rootLine;
-                }
-                $timeTracker->pull();
-            } else {
-                $message = 'No TypoScript template found!';
-                $this->logger->alert($message);
-                try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
-                        $request,
-                        $message,
-                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
-                    );
-                    throw new PropagateResponseException($response, 1533931380);
-                } catch (AbstractServerErrorException $e) {
-                    $exceptionClass = get_class($e);
-                    throw new $exceptionClass($message, 1294587218);
-                }
-            }
-        }
-
-        // No cache
-        // Set $this->no_cache TRUE if the config.no_cache value is set!
-        if ($this->config['config']['no_cache'] ?? false) {
-            $this->set_no_cache('config.no_cache is set', true);
-        }
-
-        // Auto-configure settings when a site is configured
-        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
-
-        // Hook for postProcessing the configuration array
-        $params = ['config' => &$this->config['config']];
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
-            GeneralUtility::callUserFunction($funcRef, $params, $this);
-        }
-    }
-
     /********************************************
      *
      * Further initialization and data processing
@@ -1779,7 +1990,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     public function releaseLocks()
     {
-        $this->releaseLock('pagesection');
         $this->releaseLock('pages');
     }
 
@@ -1812,10 +2022,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      */
     public function generatePage_preProcessing()
     {
-        // Same codeline as in getFromCache(). But $this->all has been changed by
-        // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
-        $this->newHash = $this->getHash();
-
         // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation.
         $this->no_cacheBeforePageGen = $this->no_cache;
     }
@@ -2401,7 +2607,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     public function getPagesTSconfig(): array
     {
         if (!is_array($this->pagesTSconfig)) {
-            $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine);
+            $matcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine);
             $this->pagesTSconfig = GeneralUtility::makeInstance(PageTsConfig::class)
                 ->getForRootLine(
                     array_reverse($this->rootLine),
@@ -2573,7 +2779,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @param string $key
      * @throws \InvalidArgumentException
      * @throws \RuntimeException
-     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
+     * @internal
      */
     protected function acquireLock($type, $key)
     {
@@ -2618,7 +2824,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @param string $type
      * @throws \InvalidArgumentException
      * @throws \RuntimeException
-     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
+     * @internal
      */
     protected function releaseLock($type)
     {
diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
index b0f12046d526aa83f33e78c2743efeaef7be0b67..131d7cd60498590303fb323dc55559a2820e2563 100644
--- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
@@ -136,7 +136,7 @@ class RequestHandler implements RequestHandlerInterface
 
             // Content generation
             $this->timeTracker->incStackPointer();
-            $this->timeTracker->push($controller->sPre, 'PAGE');
+            $this->timeTracker->push('Page generation PAGE object');
 
             $controller->content = $this->generatePageContent($controller, $request);
 
diff --git a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
index 9f18d6c1b31fb20503731713d26fa20d64b35b1a..f756753ed4960b42333f5452e8c07891aa71cf5e 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
@@ -29,26 +29,17 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
  *
  * Do all necessary preparation steps for rendering
  *
- * @internal this middleware might get removed in TYPO3 v10.x.
+ * @internal this middleware might get removed later.
  */
-class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
+final class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
 {
-    /**
-     * @var TimeTracker
-     */
-    protected $timeTracker;
-
-    public function __construct(TimeTracker $timeTracker)
-    {
-        $this->timeTracker = $timeTracker;
+    public function __construct(
+        private readonly TimeTracker $timeTracker
+    ) {
     }
 
     /**
      * Initialize TypoScriptFrontendController to the point right before rendering of the page is triggered
-     *
-     * @param ServerRequestInterface $request
-     * @param RequestHandlerInterface $handler
-     * @return ResponseInterface
      */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
@@ -58,14 +49,11 @@ class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
         // as long as TSFE throws errors with the global object, this needs to be set, but
         // should be removed later-on once TypoScript Condition Matcher is built with the current request object.
         $GLOBALS['TYPO3_REQUEST'] = $request;
-        // Get from cache
+
         $this->timeTracker->push('Get Page from cache');
-        // Locks may be acquired here
+        // Get from cache. Locks may be acquired here. After this, we should have a valid config-array ready.
         $controller->getFromCache($request);
         $this->timeTracker->pull();
-        // Get config if not already gotten
-        // After this, we should have a valid config-array ready
-        $controller->getConfigArray($request);
 
         $response = $handler->handle($request);
 
diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
index f8e909ef24e9b03f850fe9a3a90c87a499ed79f4..5308f90b257e8d4c7bf999fcf3c4f4dcf56348a8 100644
--- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
+++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php
@@ -38,18 +38,13 @@ use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
  * In addition, determineId builds up the rootline based on a valid frontend-user authentication and
  * Backend permissions if previewing.
  *
- * @internal this middleware might get removed in TYPO3 v11.0.
+ * @internal this middleware might get removed later.
  */
-class TypoScriptFrontendInitialization implements MiddlewareInterface
+final class TypoScriptFrontendInitialization implements MiddlewareInterface
 {
-    /**
-     * @var Context
-     */
-    protected $context;
-
-    public function __construct(Context $context)
-    {
-        $this->context = $context;
+    public function __construct(
+        private readonly Context $context
+    ) {
     }
 
     /**
diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
index 45a7c6ec1d21d4ce06227f75db6f61b28c76d44c..66ce9f8a88cc117cc071a61f2c82b46a932d3060 100644
--- a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
@@ -25,6 +25,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Http\Message\ServerRequestInterface;
 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\Core\SystemEnvironmentBuilder;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
@@ -279,6 +280,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
 
     /**
      * @test
+     * @todo: Turn into functional test or drop.
      */
     public function pageRendererLanguageIsSetToSiteLanguageTypo3LanguageInConstructor(): void
     {
@@ -289,6 +291,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
         $cacheManagerProphecy = $this->prophesize(CacheManager::class);
         $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal());
         $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal());
+        $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class);
+        $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal());
         GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
@@ -318,6 +322,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
 
     /**
      * @test
+     * @todo: Turn into functional test or drop
      */
     public function languageServiceIsSetUpWithSiteLanguageTypo3LanguageInConstructor(): void
     {
@@ -328,6 +333,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
         $cacheManagerProphecy = $this->prophesize(CacheManager::class);
         $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal());
         $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal());
+        $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class);
+        $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal());
         GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/'))
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
@@ -362,6 +369,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
 
     /**
      * @test
+     * @todo: Turn into functional test or drop
      */
     public function mountPointParameterContainsOnlyValidMPValues(): void
     {
@@ -372,6 +380,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
         $cacheManagerProphecy = $this->prophesize(CacheManager::class);
         $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal());
         $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal());
+        $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class);
+        $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal());
         GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
         $languageService = new LanguageService(new Locales(), new LocalizationFactory(new LanguageStore($packageManagerProphecy->reveal()), $cacheManagerProphecy->reveal()), $cacheFrontendProphecy->reveal());
         $languageServiceFactoryProphecy = $this->prophesize(LanguageServiceFactory::class);
diff --git a/typo3/sysext/frontend/ext_localconf.php b/typo3/sysext/frontend/ext_localconf.php
index 381413ec5d20609533686b9659b64f5518774880..82d8454d097b8991b867a3375cc8d83bb1a6ef11 100644
--- a/typo3/sysext/frontend/ext_localconf.php
+++ b/typo3/sysext/frontend/ext_localconf.php
@@ -2,6 +2,8 @@
 
 declare(strict_types=1);
 
+use TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Frontend\Controller\ShowImageController;
 use TYPO3\CMS\Frontend\Hooks\TreelistCacheUpdateHooks;
@@ -11,6 +13,17 @@ defined('TYPO3') or die();
 // Register eID provider for showpic
 $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['tx_cms_showpic'] = ShowImageController::class . '::processRequest';
 
+// Register TypoScript caching
+if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['typoscript'] ?? null)) {
+    $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['typoscript'] = [
+        'frontend' => PhpFrontend::class,
+        'backend' => SimpleFileBackend::class,
+        'groups' => [
+            'pages',
+        ],
+    ];
+}
+
 ExtensionManagementUtility::addUserTSConfig('
   options.saveDocView = 1
   options.saveDocNew = 1
diff --git a/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php
index c73093418adcb20731dfa1ca7ace913eae75db25..56c57a678ffa36b5bbca9916a8b4534cc312c5cc 100644
--- a/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php
+++ b/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php
@@ -31,7 +31,6 @@ class CustomCachePreset extends AbstractCustomPreset implements CustomPresetInte
     protected $configurationValues = [
         'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class,
-        'SYS/caching/cacheConfigurations/pagesection/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/imagesizes/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class,
     ];
diff --git a/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php
index cc838f2df11f13483227c7df112967b01473d2a3..681efefdce85bf858aa28b8d8d045d5fea073d39 100644
--- a/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php
+++ b/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php
@@ -37,8 +37,6 @@ class DatabaseCachePreset extends AbstractPreset
         'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/pages/options/compression' => true,
-        'SYS/caching/cacheConfigurations/pagesection/backend' => Typo3DatabaseBackend::class,
-        'SYS/caching/cacheConfigurations/pagesection/options/compression' => true,
         'SYS/caching/cacheConfigurations/imagesizes/backend' => Typo3DatabaseBackend::class,
         'SYS/caching/cacheConfigurations/imagesizes/options/compression' => true,
         'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class,
diff --git a/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php
index b241ceaa934ccb8b05f20802dc9969f5dab5c4cb..fd53b8ec4bd771bdd8930d72a1aa530325ab1844 100644
--- a/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php
+++ b/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php
@@ -38,8 +38,6 @@ class FileCachePreset extends AbstractPreset
         'SYS/caching/cacheConfigurations/hash/backend' => FileBackend::class,
         'SYS/caching/cacheConfigurations/pages/backend' => FileBackend::class,
         'SYS/caching/cacheConfigurations/pages/options/compression' => '__UNSET',
-        'SYS/caching/cacheConfigurations/pagesection/backend' => FileBackend::class,
-        'SYS/caching/cacheConfigurations/pagesection/options/compression' => '__UNSET',
         'SYS/caching/cacheConfigurations/imagesizes/backend' => SimpleFileBackend::class,
         'SYS/caching/cacheConfigurations/imagesizes/options/compression' => '__UNSET',
         'SYS/caching/cacheConfigurations/rootline/backend' => FileBackend::class,
diff --git a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
index 9709c03e724b3c7755c965901081736bc72b69f2..0f80e1efc61980738e8553e1fd6b7f03f6eb94a3 100644
--- a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
+++ b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
@@ -180,6 +180,8 @@ class SilentConfigurationUpgradeService
         // Please note that further migrations in this file are kept in order to remove the setting at the very end
         // #97797
         'GFX/processor_path_lzw',
+        // #98503
+        'SYS/caching/cacheConfigurations/pagesection',
     ];
 
     public function __construct(ConfigurationManager $configurationManager)
diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php
index e3bf0335b52dac57c860131c30a5f08b68d4ca3c..89b437662a86bef6d5339950713f006a3391ab00 100644
--- a/typo3/sysext/redirects/Classes/Service/RedirectService.php
+++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php
@@ -350,6 +350,12 @@ class RedirectService implements LoggerAwareInterface
      * - TSFE->cObj
      *
      * So a link to a page can be generated.
+     *
+     * @todo: This messes quite a bit with dependencies here. RedirectService is called by an early middleware
+     *        *before* TSFE has been set up at all. The code thus has to hop through various loops later middlewares
+     *        would usually do. The overall scenario of needing a partially set up TSFE for target redirect calculation
+     *        is quite unfortunate here and should be sorted out differently by further refactoring the link building
+     *        and reducing TSFE dependencies.
      */
     protected function bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): TypoScriptFrontendController
     {
@@ -363,7 +369,8 @@ class RedirectService implements LoggerAwareInterface
         );
         $controller->determineId($originalRequest);
         $controller->calculateLinkVars($queryParams);
-        $controller->getConfigArray($originalRequest);
+        $controller->getFromCache($originalRequest);
+        $controller->releaseLocks();
         $controller->newCObj($originalRequest);
         if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
             $GLOBALS['TSFE'] = $controller;
diff --git a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
index 64ef8554958e652a436e38f5d8b7305fa1f446fb..89bd778cc5068cb5a714100bf862f7307bb2ef1e 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
@@ -25,10 +25,12 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface;
 use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
 use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
 use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstConstantCommentVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder;
 use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
@@ -45,6 +47,7 @@ class ConstantEditorController extends AbstractTemplateModuleController
 {
     public function __construct(
         protected readonly ModuleTemplateFactory $moduleTemplateFactory,
+        private readonly SysTemplateRepository $sysTemplateRepository,
         private readonly TreeBuilder $treeBuilder,
         private readonly IncludeTreeTraverser $treeTraverser,
         private readonly AstTraverser $astTraverser,
@@ -126,7 +129,10 @@ class ConstantEditorController extends AbstractTemplateModuleController
 
         // Build the constant include tree
         $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
-        $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid);
+        /** @var SiteInterface|null $site */
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid);
+        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site);
         $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class);
         $this->treeTraverser->resetVisitors();
         $this->treeTraverser->addVisitor($constantAstBuilderVisitor);
@@ -222,7 +228,10 @@ class ConstantEditorController extends AbstractTemplateModuleController
         }
 
         $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
-        $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid);
+        /** @var SiteInterface|null $site */
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid);
+        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site);
         $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class);
         $this->treeTraverser->resetVisitors();
         $this->treeTraverser->addVisitor($constantAstBuilderVisitor);
diff --git a/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php b/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php
index b4f9aa20034005e59760ac7a72fadd2fa3cacf59..449a795d67236812cc7e36a105242af6a7ae2a45 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php
@@ -28,11 +28,13 @@ use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
 use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
 use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
 use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
 use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstSortChildrenVisitor;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder;
@@ -57,6 +59,7 @@ final class ObjectBrowserController extends AbstractTemplateModuleController
     public function __construct(
         private readonly ModuleTemplateFactory $moduleTemplateFactory,
         private readonly FlashMessageService $flashMessageService,
+        private readonly SysTemplateRepository $sysTemplateRepository,
         private readonly IncludeTreeTraverser $treeTraverser,
         private readonly ConditionVerdictAwareIncludeTreeTraverser $treeTraverserConditionVerdictAware,
         private readonly TreeBuilder $treeBuilder,
@@ -137,8 +140,12 @@ final class ObjectBrowserController extends AbstractTemplateModuleController
         $displayComments = $moduleData->get('displayComments');
         $searchValue = $moduleData->get('searchValue');
 
+        /** @var SiteInterface|null $site */
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid);
+
         // Build the constant include tree
-        $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid);
+        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site);
         // Set enabled conditions in constant include tree
         $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, $parsedBody);
         $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);
@@ -192,8 +199,8 @@ final class ObjectBrowserController extends AbstractTemplateModuleController
 
         // Flatten constant AST. Needed for setup condition display and setup AST constant substitution.
         $flattenedConstants = $constantAst->flatten();
-        // Build the setup include tree, note this uses the 'cached' variant from the constant run above to suppress db calls.
-        $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', true);
+        // Build the setup include tree
+        $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site);
         // Set enabled conditions in setup include tree and let it handle constant substitutions in setup conditions.
         $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, $parsedBody, $flattenedConstants);
         $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);
@@ -313,9 +320,13 @@ final class ObjectBrowserController extends AbstractTemplateModuleController
             }
         }
 
+        /** @var SiteInterface|null $site */
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid);
+
         // Get current value of to-edit object path
         // Build the constant include tree
-        $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid);
+        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site);
         // Set enabled conditions in constant include tree
         $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, null);
         $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);
@@ -333,7 +344,7 @@ final class ObjectBrowserController extends AbstractTemplateModuleController
             $currentValue = $flattenedConstants[$currentObjectPath] ?? '';
         } else {
             // Build the setup include tree
-            $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', false, $selectedTemplateUid);
+            $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site);
             // Set enabled conditions in setup include tree
             $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, null, $flattenedConstants);
             $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);
diff --git a/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php b/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php
index 41817b12922c123cb3b551806670aabb21cae6fc..34e9ecf758fcd9711cdebcba66d2b193c0383935 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php
@@ -25,7 +25,9 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Http\RedirectResponse;
 use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
 use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder;
@@ -50,6 +52,7 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController
     public function __construct(
         private readonly PageRenderer $pageRenderer,
         private readonly ModuleTemplateFactory $moduleTemplateFactory,
+        private readonly SysTemplateRepository $sysTemplateRepository,
         private readonly IncludeTreeTraverser $treeTraverser,
         private readonly ConditionVerdictAwareIncludeTreeTraverser $treeTraverserConditionVerdictAware,
         private readonly TreeBuilder $treeBuilder,
@@ -107,8 +110,12 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController
             $this->pageRenderer->loadJavaScriptModule('@typo3/t3editor/element/code-mirror-element.js');
         }
 
+        /** @var SiteInterface|null $site */
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid);
+
         // Build the constant include tree
-        $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid);
+        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site);
         // Set enabled conditions in constant include tree
         $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, $parsedBody);
         $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);
@@ -124,8 +131,8 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController
         $constantAst = $constantAstBuilderVisitor->getAst();
         $flattenedConstants = $constantAst->flatten();
 
-        // Build the setup include tree, note this uses the 'cached' variant from the constant run above to suppress db calls.
-        $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', true);
+        // Build the setup include tree
+        $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site);
         // Set enabled conditions in setup include tree and let it handle constant substitutions in setup conditions.
         $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, $parsedBody, $flattenedConstants);
         $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class);