From bd46974b6ae513107ee10094dbe7e3da38184ca4 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Fri, 29 Mar 2024 22:24:36 +0100
Subject: [PATCH] [FEATURE] Optimized integration of Page Rendering via Fluid

This change adds a new cObject "PAGEVIEW" which has a much simpler
usage than "FLUIDTEMPLATE", because it is meant to render
a full page like this:

page = PAGE
page.10 = PAGEVIEW
page.10.paths.100 = EXT:site_extension/Resources/Private/Templates

Nothing else is needed by default, as
a) the layoutRootPaths are resolved to be meant to be under "Templates/Layouts" or "Templates/layouts"
b) the partialRootPaths are resolved to be meant to be under "Templates/Partials" or "Templates/partials"
automatically are added.

The name of the template is resolved from the Page Layout (Backend Layout)
identifier, so add a "Templates/Pages/Mypage.html" if there is a backend layout with "mypage" added.

In addition, the reserved variables
* site (Site object)
* language (Site Language object)
* page (Page object)
* settings (Page / TypoScript Settings)

are injected automatically, so the View can work with this information directly.

template folders for "pages", "layouts" and "partials" can start
with an upper-case or lower-case in order to ensure
maximum compatibility and avoid common mistakes.

Why do we do this?

We've found that FLUIDTEMPLATE is often used for page rendering,
but lacks specific usages for pages to be worked directly without
the understanding of Fluid internals and TypoScript code.

This functionality will be further adapted to be used for further rendering of
custom Content Elements and Content Blocks which will follow in
subsequent steps.

For this reason this API is considered experimental until TYPO3 v13 LTS.

Resolves: #103504
Releases: main
Change-Id: Ieb2b665945ad671fe7e7c195b3201dbc0dbd7076
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83626
Tested-by: Benjamin Kott <benjamin.kott@outlook.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../core/Classes/Page/PageLayoutResolver.php  |   9 +
 ...eature-103504-NewContentObjectPageView.rst | 153 ++++++++++++++
 .../ContentObject/PageViewContentObject.php   | 194 ++++++++++++++++++
 .../frontend/Configuration/Services.yaml      |   5 +
 .../Fixtures/FluidPage/pages.csv              |   4 +
 .../PageViewContentObjectTest.php             |  82 ++++++++
 .../Configuration/TypoScript/plain.typoscript |   9 +
 .../Configuration/page.tsconfig               |  63 ++++++
 .../Resources/Private/Language/fr.page.xlf    |  12 ++
 .../Resources/Private/Language/page.xlf       |  11 +
 .../Private/Templates/Layouts/Default.html    |   9 +
 .../Private/Templates/Pages/Standard.html     |   5 +
 .../layouts/Default.html                      |   9 +
 .../pages/Standard.html                       |   5 +
 .../test_fluidpagerendering/composer.json     |  14 ++
 .../test_fluidpagerendering/ext_emconf.php    |  18 ++
 16 files changed, 602 insertions(+)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.1/Feature-103504-NewContentObjectPageView.rst
 create mode 100644 typo3/sysext/frontend/Classes/ContentObject/PageViewContentObject.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/ContentObject/Fixtures/FluidPage/pages.csv
 create mode 100644 typo3/sysext/frontend/Tests/Functional/ContentObject/PageViewContentObjectTest.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/page.tsconfig
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/fr.page.xlf
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/page.xlf
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Layouts/Default.html
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Pages/Standard.html
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/layouts/Default.html
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/pages/Standard.html
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/composer.json
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/ext_emconf.php

diff --git a/typo3/sysext/core/Classes/Page/PageLayoutResolver.php b/typo3/sysext/core/Classes/Page/PageLayoutResolver.php
index df7c330eb4a3..1d5189179514 100644
--- a/typo3/sysext/core/Classes/Page/PageLayoutResolver.php
+++ b/typo3/sysext/core/Classes/Page/PageLayoutResolver.php
@@ -131,4 +131,13 @@ class PageLayoutResolver
         }
         return $selectedLayout;
     }
+
+    public function getLayoutIdentifierForPageWithoutPrefix(array $page, array $rootLine): string
+    {
+        $selectedLayout = $this->getLayoutIdentifierForPage($page, $rootLine);
+        if (str_contains($selectedLayout, '__')) {
+            return explode('__', $selectedLayout, 2)[1] ?? '';
+        }
+        return $selectedLayout;
+    }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103504-NewContentObjectPageView.rst b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103504-NewContentObjectPageView.rst
new file mode 100644
index 000000000000..0c5ac05ba73a
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103504-NewContentObjectPageView.rst
@@ -0,0 +1,153 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-103504-1712041725:
+
+=============================================
+Feature: #103504 - New ContentObject PAGEVIEW
+=============================================
+
+See :issue:`103504`
+
+Description
+===========
+
+A new Content Object for TypoScript :typoscript:`PAGEVIEW` has been added.
+
+This cObject is mainly intended for rendering a full page in the TYPO3 Frontend
+with fewer configuration options over the generic :typoscript:`FLUIDTEMPLATE`
+cObject.
+
+A basic usage of the :typoscript:`PAGEVIEW` cObject is as follows:
+
+.. code-block:: typoscript
+
+    page = PAGE
+    page.10 = PAGEVIEW
+    page.10.paths.100 = EXT:mysite/Resources/Private/Templates/
+
+:typoscript:`PAGEVIEW` wires certain parts automatically:
+
+1. The name of the used page layout (Backend Layout) is resolved automatically.
+
+If a page has a layout named "with_sidebar", the template file is then resolved
+to :file:`EXT:mysite/Resources/Private/Templates/Pages/With_sidebar.html`.
+
+2. Fluid features for Layouts and Partials are wired automatically, thus they
+can be placed into :file:`EXT:mysite/Resources/Private/Templates/Layouts/`
+and :file:`EXT:mysite/Resources/Private/Templates/Partials/`.
+
+In order to reduce the burdon for integrators, the folder names for "pages",
+"layouts" and "partials" can start with lower-case or upper-case.
+
+3. Default variables are available in the Fluid template:
+
+- :typoscript:`settings` - contains all TypoScript settings (= Constants)
+- :typoscript:`site` - the current Site object
+- :typoscript:`language` - the current Site Language object
+- :typoscript:`page` - the current Page record as object
+
+There is no special Extbase resolving done for the templates.
+
+Before
+------
+
+.. code-block:: typoscript
+
+    page = PAGE
+    page {
+        10 = FLUIDTEMPLATE
+        10 {
+            templateName = TEXT
+            templateName {
+                stdWrap {
+                    cObject = TEXT
+                    cObject {
+                        data = levelfield:-2, backend_layout_next_level, slide
+                        override {
+                            field = backend_layout
+                        }
+                        split {
+                            token = pagets__
+                            1 {
+                                current = 1
+                                wrap = |
+                            }
+                        }
+                    }
+                    ifEmpty = Standard
+                }
+            }
+
+            templateRootPaths {
+                100 = {$plugin.tx_mysite.paths.templates}
+            }
+
+            partialRootPaths {
+                100 = {$plugin.tx_mysite.paths.partials}
+            }
+
+            layoutRootPaths {
+                100 = {$plugin.tx_mysite.paths.layouts}
+            }
+
+            variables {
+                pageUid = TEXT
+                pageUid.data = page:uid
+
+                pageTitle = TEXT
+                pageTitle.data = page:title
+
+                pageSubtitle = TEXT
+                pageSubtitle.data = page:subtitle
+
+                parentPageTitle = TEXT
+                parentPageTitle.data = levelfield:-1:title
+            }
+
+            dataProcessing {
+                10 = menu
+                10.as = mainMenu
+            }
+        }
+    }
+
+After
+-----
+
+.. code-block:: typoscript
+
+    page = PAGE
+    page {
+        10 = PAGEVIEW
+        10 {
+            paths {
+                100 = {$plugin.tx_mysite.paths.templates}
+            }
+            variables {
+                parentPageTitle = TEXT
+                parentPageTitle.data = levelfield:-1:title
+            }
+            dataProcessing {
+                10 = menu
+                10.as = mainMenu
+            }
+        }
+    }
+
+In Fluid, the pageUid is available as :html:`{page.uid}` and pageTitle
+as :html:`{page.title}`.
+
+Impact
+======
+
+Creating new page templates based on Fluid follows conventions in order to
+reduce the amount of TypoScript needed to render a page in the TYPO3 Frontend.
+
+Sane defaults are applied, variables and settings are available at any time.
+
+.. note::
+
+    This cObject is marked as experimental until TYPO3 v13 LTS as some
+    functionality will be added.
+
+.. index:: TypoScript, ext:frontend
diff --git a/typo3/sysext/frontend/Classes/ContentObject/PageViewContentObject.php b/typo3/sysext/frontend/Classes/ContentObject/PageViewContentObject.php
new file mode 100644
index 000000000000..01116c70bb2b
--- /dev/null
+++ b/typo3/sysext/frontend/Classes/ContentObject/PageViewContentObject.php
@@ -0,0 +1,194 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Frontend\ContentObject;
+
+use TYPO3\CMS\Core\Domain\Page;
+use TYPO3\CMS\Core\Page\PageLayoutResolver;
+use TYPO3\CMS\Core\TypoScript\TypoScriptService;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
+
+/**
+ * PAGEVIEW Content Object.
+ *
+ * Built to render a full page with Fluid, and does the following
+ * - uses the template from the given Page Layout / Backend Layout of the current page in a folder "pages/mylayout.html"
+ * - paths are resolved from "paths." configuration
+ * - automatically adds templateRootPaths to the layoutRootPaths and partialRootPaths as well with a suffix "layouts/" and "partials/"
+ * - injects pageInformation, site and siteLanguage (= language) as variables by default
+ * - adds all page settings (= TypoScript constants) into the settings variable of the View
+ *
+ * In contrast to FLUIDTEMPLATE, by design this cObject
+ * - does not handle custom layoutRootPaths and partialRootPaths
+ * - does not handle Extbase specialities
+ * - does not handle HeaderAssets and FooterAssets
+ * - does not handle "templateName.", "template." and "file." resolving from cObject
+ *
+ * @internal this cObject is considered experimental until TYPO3 v13 LTS
+ */
+class PageViewContentObject extends AbstractContentObject
+{
+    protected array $reservedVariables = ['site', 'language', 'page'];
+
+    public function __construct(
+        protected readonly ContentDataProcessor $contentDataProcessor,
+        protected readonly StandaloneView $view,
+        protected readonly TypoScriptService $typoScriptService,
+        protected readonly PageLayoutResolver $pageLayoutResolver,
+    ) {}
+
+    /**
+     * Rendering the cObject, PAGEVIEW
+     *
+     * Configuration properties:
+     *  - paths array to template files
+     *  - variables array of cObjects, the keys are the variable names in fluid
+     *  - dataProcessing array of data processors which are classes to manipulate $data
+     *
+     * Example:
+     * page.10 = PAGEVIEW
+     * page.10.paths.10 = EXT:site_configuration/Resources/Private/Templates/
+     * page.10.variables {
+     *   mylabel = TEXT
+     *   mylabel.value = Label from TypoScript
+     * }
+     *
+     * @param array $conf Array of TypoScript properties
+     * @return string The HTML output
+     */
+    public function render($conf = []): string
+    {
+        if (!is_array($conf)) {
+            $conf = [];
+        }
+        $this->view->setRequest($this->request);
+
+        $this->setTemplate($conf);
+        $this->assignSettings();
+        $variables = $this->getContentObjectVariables($conf);
+        $variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
+
+        $this->view->assignMultiple($variables);
+
+        return $this->view->render();
+    }
+
+    protected function setTemplate(array $conf): void
+    {
+        if (is_array($conf['paths.'] ?? false) && $conf['paths.'] !== []) {
+            $this->view->setTemplateRootPaths($conf['paths.']);
+            $this->setLayoutPaths();
+            $this->setPartialPaths();
+        }
+        // Fetch the Fluid template by the name of the Page Layout and underneath "Pages"
+        $pageInformationObject = $this->request->getAttribute('frontend.page.information');
+        $pageLayoutName = $this->pageLayoutResolver->getLayoutIdentifierForPageWithoutPrefix(
+            $pageInformationObject->getPageRecord(),
+            $pageInformationObject->getRootLine()
+        );
+
+        $this->view->getRenderingContext()->setControllerAction($pageLayoutName);
+        $this->view->getRenderingContext()->setControllerName('pages');
+        // Also allow an upper case folder as fallback
+        if (!$this->view->hasTemplate()) {
+            $this->view->getRenderingContext()->setControllerName('Pages');
+        }
+        // If template still does not exist, rendering is not possible.
+        if (!$this->view->hasTemplate()) {
+            $configuredTemplateRootPaths = implode(', ', $this->view->getTemplateRootPaths());
+            throw new ContentRenderingException(
+                'Could not find template source for "pages/' . $pageLayoutName . '".'
+                . ' Configured templateRootPaths: ' . $configuredTemplateRootPaths,
+                1711797936
+            );
+        }
+    }
+
+    /**
+     * Set layout root paths from the template paths
+     */
+    protected function setLayoutPaths(): void
+    {
+        // Define the default root paths to be located in the base paths under "layouts/" subfolder
+        // Handle unix paths to allow upper-case folders as well
+        $templateRootPathsLowerCase = array_map(static fn(string $path): string => $path . 'layouts/', $this->view->getTemplateRootPaths());
+        $templateRootPathsUpperCase = array_map(static fn(string $path): string => $path . 'Layouts/', $this->view->getTemplateRootPaths());
+        $layoutPaths = array_merge($templateRootPathsUpperCase, $templateRootPathsLowerCase);
+        if ($layoutPaths !== []) {
+            $this->view->setLayoutRootPaths($layoutPaths);
+        }
+    }
+
+    /**
+     * Set partial root path from the template root paths
+     */
+    protected function setPartialPaths(): void
+    {
+        // Define the default root paths to be located in the base paths under "partials/" subfolder
+        // Handle unix paths to allow upper-case folders as well
+        $templateRootPathsLowerCase = array_map(static fn(string $path): string => $path . 'partials/', $this->view->getTemplateRootPaths());
+        $templateRootPathsUpperCase = array_map(static fn(string $path): string => $path . 'Partials/', $this->view->getTemplateRootPaths());
+        $partialPaths = array_merge($templateRootPathsUpperCase, $templateRootPathsLowerCase);
+        if ($partialPaths !== []) {
+            $this->view->setPartialRootPaths($partialPaths);
+        }
+    }
+
+    /**
+     * Compile rendered content objects in variables array ready to assign to the view
+     *
+     * @param array $conf Configuration array
+     * @return array the variables to be assigned
+     * @throws \InvalidArgumentException
+     */
+    protected function getContentObjectVariables(array $conf): array
+    {
+        $variables = [
+            'site' => $this->request->getAttribute('site'),
+            'language' => $this->request->getAttribute('language'),
+            'page' => new Page($this->request->getAttribute('frontend.page.information')->getPageRecord()),
+        ];
+        // Accumulate the variables to be process and loop them through cObjGetSingle
+        if (is_array($conf['variables.'] ?? false) && $conf['variables.'] !== []) {
+            foreach ($conf['variables.'] as $variableName => $cObjType) {
+                if (!is_string($cObjType)) {
+                    continue;
+                }
+                if (in_array($variableName, $this->reservedVariables, true)) {
+                    throw new \InvalidArgumentException(
+                        'Cannot use reserved name "' . $variableName . '" as variable name in PAGEVIEW.',
+                        1711748615
+                    );
+                }
+                $cObjConf = $conf['variables.'][$variableName . '.'] ?? [];
+                $variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
+            }
+        }
+        return $variables;
+    }
+
+    /**
+     * Set any TypoScript settings to the view, which take precedence over the page-specific settings.
+     */
+    protected function assignSettings(): void
+    {
+        $pageSettings = $this->request->getAttribute('frontend.typoscript')->getSettingsTree()->toArray();
+        $pageSettings = $this->typoScriptService->convertTypoScriptArrayToPlainArray($pageSettings);
+        $this->view->assign('settings', $pageSettings);
+    }
+}
diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml
index 52305edbb6a7..54cfbd6bf423 100644
--- a/typo3/sysext/frontend/Configuration/Services.yaml
+++ b/typo3/sysext/frontend/Configuration/Services.yaml
@@ -133,6 +133,11 @@ services:
       - name: frontend.contentobject
         identifier: 'FLUIDTEMPLATE'
 
+  TYPO3\CMS\Frontend\ContentObject\PageViewContentObject:
+    tags:
+      - name: frontend.contentobject
+        identifier: 'PAGEVIEW'
+
   TYPO3\CMS\Frontend\ContentObject\ScalableVectorGraphicsContentObject:
     tags:
       - name: frontend.contentobject
diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/Fixtures/FluidPage/pages.csv b/typo3/sysext/frontend/Tests/Functional/ContentObject/Fixtures/FluidPage/pages.csv
new file mode 100644
index 000000000000..57f4b7c92687
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/Fixtures/FluidPage/pages.csv
@@ -0,0 +1,4 @@
+pages
+,uid,pid,slug,title,sys_language_uid,l10n_parent,backend_layout,TSconfig
+,1,0,/,Fluid Root Page,0,0,pagets__Standard,@import "EXT:test_fluidpagerendering/Configuration/TypoScript/plain.typoscript"
+,2,0,/,Fluid Root Page FR,1,1,pagets__Standard,@import "EXT:test_fluidpagerendering/Configuration/TypoScript/plain.typoscript"
diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/PageViewContentObjectTest.php b/typo3/sysext/frontend/Tests/Functional/ContentObject/PageViewContentObjectTest.php
new file mode 100644
index 000000000000..7363c03b8ebf
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/PageViewContentObjectTest.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Frontend\Tests\Functional\ContentObject;
+
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class PageViewContentObjectTest extends FunctionalTestCase
+{
+    use SiteBasedTestTrait;
+
+    protected const LANGUAGE_PRESETS = [
+        'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en-US'],
+        'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr-FR'],
+    ];
+    protected const ROOT_PAGE_ID = 1;
+
+    protected array $testExtensionsToLoad = [
+        'typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering',
+    ];
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/FluidPage/pages.csv');
+        $this->writeSiteConfiguration(
+            'pageview_template',
+            $this->buildSiteConfiguration(self::ROOT_PAGE_ID, '/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/'),
+                $this->buildLanguageConfiguration('FR', '/fr'),
+            ],
+        );
+    }
+
+    #[Test]
+    public function renderWorksWithPlainRenderingInMultipleLanguages(): void
+    {
+        $this->setUpFrontendRootPage(
+            self::ROOT_PAGE_ID,
+            [
+                'EXT:frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript',
+            ]
+        );
+        $response = $this->executeFrontendSubRequest((new InternalRequest())->withPageId(self::ROOT_PAGE_ID));
+        self::assertStringContainsString('You are on page Fluid Root Page', (string)$response->getBody());
+        self::assertStringContainsString('This is a standard page with no content.', (string)$response->getBody());
+        $response = $this->executeFrontendSubRequest((new InternalRequest())->withPageId(self::ROOT_PAGE_ID)->withLanguageId(1));
+        self::assertStringContainsString('Vous êtes à la page Fluid Root Page FR', (string)$response->getBody());
+    }
+
+    #[Test]
+    public function renderWorksWithPlainRenderingWithLowerCasePaths(): void
+    {
+        $this->setUpFrontendRootPage(
+            self::ROOT_PAGE_ID,
+            [
+                'EXT:frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript',
+            ]
+        );
+        $response = $this->executeFrontendSubRequest((new InternalRequest())->withPageId(self::ROOT_PAGE_ID)->withQueryParameter('type', 123));
+        self::assertStringContainsString('You are on page Fluid Root Page', (string)$response->getBody());
+        self::assertStringContainsString('This is a standard page with no content.', (string)$response->getBody());
+    }
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript
new file mode 100644
index 000000000000..e979d3035489
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/TypoScript/plain.typoscript
@@ -0,0 +1,9 @@
+page = PAGE
+page.10 = PAGEVIEW
+page.10.paths.100 = EXT:test_fluidpagerendering/Resources/Private/Templates/
+
+lowercasepage = PAGE
+lowercasepage.typeNum = 123
+lowercasepage.10 = PAGEVIEW
+lowercasepage.10.paths.100 = EXT:test_fluidpagerendering/Resources/Private/templates_in_lowercase/
+
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/page.tsconfig b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/page.tsconfig
new file mode 100644
index 000000000000..9862ffc3126f
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Configuration/page.tsconfig
@@ -0,0 +1,63 @@
+mod.web_layout.BackendLayouts {
+  Standard {
+    title = Standard Test Page Layout
+    config {
+      backend_layout {
+        colCount = 3
+        rowCount = 3
+        rows {
+          1 {
+            columns {
+              1 {
+                name = Header
+                colspan = 3
+                colPos = 1
+                identifier = header
+              }
+            }
+          }
+          2 {
+            columns {
+              1 {
+                name = Main
+                colspan = 2
+                colPos = 0
+                identifier = main
+              }
+              2 {
+                name = Aside
+                rowspan = 2
+                colPos = 2
+                identifier = aside
+                slideMode = collect
+              }
+            }
+          }
+          3 {
+            columns {
+              1 {
+                name = Main Left
+                colPos = 5
+                identifier = mainLeft
+              }
+              2 {
+                name = Main Right
+                colPos = 6
+                identifier = mainRight
+              }
+            }
+          }
+          4 {
+            columns {
+              1 {
+                name = Footer
+                colspan = 3
+                colPos = 4
+                identifier = footer
+              }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/fr.page.xlf b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/fr.page.xlf
new file mode 100644
index 000000000000..a708ca2a4859
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/fr.page.xlf
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:test_fluidpagerendering/Resources/Private/Language/page.xlf" date="2023-12-21T11:57:33Z" product-name="test_fluidpagerendering">
+		<header/>
+		<body>
+			<trans-unit id="headline" resname="headline">
+				<source>You are on page</source>
+				<target>Vous êtes à la page</target>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/page.xlf b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/page.xlf
new file mode 100644
index 000000000000..4fda1cc9a0d7
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Language/page.xlf
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:test_fluidpagerendering/Resources/Private/Language/page.xlf" date="2023-12-21T11:57:33Z" product-name="test_fluidpagerendering">
+		<header/>
+		<body>
+			<trans-unit id="headline" resname="headline">
+				<source>You are on page</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Layouts/Default.html b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Layouts/Default.html
new file mode 100644
index 000000000000..e3632c8bca2d
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Layouts/Default.html
@@ -0,0 +1,9 @@
+<div class="container">
+    <header>
+        <h1><f:translate key="LLL:EXT:test_fluidpagerendering/Resources/Private/Language/page.xlf:headline" /> {page.title}</h1>
+    </header>
+</div>
+
+<main>
+    <f:render section="content" />
+</main>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Pages/Standard.html b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Pages/Standard.html
new file mode 100644
index 000000000000..85e9054a7737
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/Templates/Pages/Standard.html
@@ -0,0 +1,5 @@
+<f:layout name="Default" />
+
+<f:section name="content">
+    This is a standard page with no content.
+</f:section>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/layouts/Default.html b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/layouts/Default.html
new file mode 100644
index 000000000000..e3632c8bca2d
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/layouts/Default.html
@@ -0,0 +1,9 @@
+<div class="container">
+    <header>
+        <h1><f:translate key="LLL:EXT:test_fluidpagerendering/Resources/Private/Language/page.xlf:headline" /> {page.title}</h1>
+    </header>
+</div>
+
+<main>
+    <f:render section="content" />
+</main>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/pages/Standard.html b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/pages/Standard.html
new file mode 100644
index 000000000000..85e9054a7737
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/Resources/Private/templates_in_lowercase/pages/Standard.html
@@ -0,0 +1,5 @@
+<f:layout name="Default" />
+
+<f:section name="content">
+    This is a standard page with no content.
+</f:section>
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/composer.json b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/composer.json
new file mode 100644
index 000000000000..376ff524e21c
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/composer.json
@@ -0,0 +1,14 @@
+{
+	"name": "typo3tests/fixture-fluidpagerendering-test",
+	"type": "typo3-cms-extension",
+	"description": "Full Page Rendering via Fluid Test",
+	"license": "GPL-2.0-or-later",
+	"require": {
+		"typo3/cms-core": "13.1.*@dev"
+	},
+	"extra": {
+		"typo3/cms": {
+			"extension-key": "test_fluidpagerendering"
+		}
+	}
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/ext_emconf.php b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/ext_emconf.php
new file mode 100644
index 000000000000..30bc59f5715c
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluidpagerendering/ext_emconf.php
@@ -0,0 +1,18 @@
+<?php
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'Fluidpage Rendering Test',
+    'description' => 'Fluidpage Rendering Test',
+    'category' => 'example',
+    'version' => '13.1.0',
+    'state' => 'stable',
+    'author' => 'Benni Mack',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '13.1.0',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
-- 
GitLab