[FEATURE] Add PageTitle API 81/57881/31
authorRichard Haeser <richard@maxserv.com>
Sun, 12 Aug 2018 17:31:24 +0000 (19:31 +0200)
committerAndreas Fernandez <a.fernandez@scripting-base.de>
Sat, 1 Sep 2018 16:16:17 +0000 (18:16 +0200)
It is now possible to set the title tag of a page by using
the PageTitle API. You can register your own providers and
set the priority of the provider so you are in control when
multiple extensions of a page trying to set the title.

Resolves: #85678
Releases: master
Change-Id: I1b0314f96b6af7bdad94b9865d2e2525b715d5c3
Reviewed-on: https://review.typo3.org/57881
Reviewed-by: Richard Haeser <richard@maxserv.com>
Tested-by: Richard Haeser <richard@maxserv.com>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
16 files changed:
typo3/sysext/core/Classes/PageTitle/AbstractPageTitleProvider.php [new file with mode: 0644]
typo3/sysext/core/Classes/PageTitle/AltPageTitleProvider.php [new file with mode: 0644]
typo3/sysext/core/Classes/PageTitle/PageTitleProviderInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/PageTitle/PageTitleProviderManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/PageTitle/RecordPageTitleProvider.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-ConfigTitleTagFunction.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-TsfeAltPageTitle.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-85678-AddAPIForTitleTags.rst [new file with mode: 0644]
typo3/sysext/core/ext_localconf.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Tests/Functional/Fixtures/pages-title-tag.xml [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/Fixtures/pages.xml
typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/TitleTagRenderingTest.typoscript [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/Rendering/TitleTagRenderingTest.php [new file with mode: 0644]
typo3/sysext/seo/Classes/PageTitle/SeoTitlePageTitleProvider.php [new file with mode: 0644]
typo3/sysext/seo/ext_localconf.php

diff --git a/typo3/sysext/core/Classes/PageTitle/AbstractPageTitleProvider.php b/typo3/sysext/core/Classes/PageTitle/AbstractPageTitleProvider.php
new file mode 100644 (file)
index 0000000..41664fa
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\PageTitle;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+
+/**
+ * Abstract for PageTitleProviders
+ */
+abstract class AbstractPageTitleProvider implements PageTitleProviderInterface, SingletonInterface
+{
+    /**
+     * @var string
+     */
+    protected $title = '';
+
+    /**
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+}
diff --git a/typo3/sysext/core/Classes/PageTitle/AltPageTitleProvider.php b/typo3/sysext/core/Classes/PageTitle/AltPageTitleProvider.php
new file mode 100644 (file)
index 0000000..5a0dac9
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\PageTitle;
+
+/*
+ * 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!
+ */
+
+/**
+ * Class to handle $GLOBALS['TSFE']->altPageTitle as input for the page title
+ */
+class AltPageTitleProvider extends AbstractPageTitleProvider
+{
+    /**
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        if (!empty($GLOBALS['TSFE']->altPageTitle)) {
+            trigger_error('This $GLOBALS[\'TSFE\']->altPageTitle is deprecated and will be removed in TYPO3 v10. Please use the TitleTag API to set the title tag', E_USER_DEPRECATED);
+
+            return $GLOBALS['TSFE']->altPageTitle;
+        }
+
+        return '';
+    }
+}
diff --git a/typo3/sysext/core/Classes/PageTitle/PageTitleProviderInterface.php b/typo3/sysext/core/Classes/PageTitle/PageTitleProviderInterface.php
new file mode 100644 (file)
index 0000000..732505f
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\PageTitle;
+
+/*
+ * 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!
+ */
+
+/**
+ * Interface for PageTitleProviders with the methods that are needed by the PageTitleProviderManager
+ */
+interface PageTitleProviderInterface
+{
+    public function getTitle(): string;
+}
diff --git a/typo3/sysext/core/Classes/PageTitle/PageTitleProviderManager.php b/typo3/sysext/core/Classes/PageTitle/PageTitleProviderManager.php
new file mode 100644 (file)
index 0000000..9178367
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\PageTitle;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Cache\Backend\AbstractBackend;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\TypoScript\TypoScriptService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+
+/**
+ * This class will take care of the different providers and returns the title with the highest priority
+ */
+class PageTitleProviderManager implements SingletonInterface
+{
+    /**
+     * @var \TYPO3\CMS\Core\Cache\Backend\AbstractBackend
+     */
+    protected $pageCache;
+
+    public function __construct()
+    {
+        $this->initCaches();
+    }
+
+    /**
+     * @return string
+     * @throws \TYPO3\CMS\Core\Cache\Exception
+     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
+     */
+    public function getTitle(): string
+    {
+        $pageTitle = '';
+
+        $titleProviders = $this->getPageTitleProviderConfiguration();
+        $titleProviders =  $this->setProviderOrder($titleProviders);
+
+        $orderedTitleProviders = GeneralUtility::makeInstance(DependencyOrderingService::class)
+            ->orderByDependencies($titleProviders);
+
+        foreach ($orderedTitleProviders as $provider => $configuration) {
+            $cacheIdentifier =  $this->getTypoScriptFrontendController()->newHash . '-titleTag-' . $provider;
+            if ($this->pageCache instanceof AbstractBackend &&
+                $pageTitle = $this->pageCache->get($cacheIdentifier)
+            ) {
+                break;
+            }
+            if (class_exists($configuration['provider']) && is_subclass_of($configuration['provider'], PageTitleProviderInterface::class)) {
+                /** @var PageTitleProviderInterface $titleProviderObject */
+                $titleProviderObject = GeneralUtility::makeInstance($configuration['provider']);
+                if ($pageTitle = $titleProviderObject->getTitle()) {
+                    $this->pageCache->set(
+                        $cacheIdentifier,
+                        $pageTitle,
+                        ['pageTitle_' . $this->getTypoScriptFrontendController()->page['uid']]
+                    );
+                    break;
+                }
+            }
+        }
+
+        return $pageTitle;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+     */
+    private function getTypoScriptFrontendController(): TypoScriptFrontendController
+    {
+        return $GLOBALS['TSFE'];
+    }
+
+    /**
+     * Get the TypoScript configuration for pageTitleProviders
+     * @return array
+     */
+    private function getPageTitleProviderConfiguration(): array
+    {
+        $typoscriptService = GeneralUtility::makeInstance(TypoScriptService::class);
+        $config = $typoscriptService->convertTypoScriptArrayToPlainArray(
+            $this->getTypoScriptFrontendController()->config['config'] ?? []
+        );
+
+        return $config['pageTitleProviders'] ?? [];
+    }
+
+    /**
+     * Initializes the caching system.
+     */
+    protected function initCaches(): void
+    {
+        try {
+            $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_pages');
+        } catch (NoSuchCacheException $e) {
+            // Intended fall-through
+        }
+    }
+
+    /**
+     * @param array $orderInformation
+     * @return string[]
+     * @throws \UnexpectedValueException
+     */
+    protected function setProviderOrder(array $orderInformation): array
+    {
+        foreach ($orderInformation as $provider => &$configuration) {
+            if (isset($configuration['before'])) {
+                if (is_string($configuration['before'])) {
+                    $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true);
+                } elseif (!is_array($configuration['before'])) {
+                    throw new \UnexpectedValueException(
+                        'The specified "before" order configuration for provider "' . $provider . '" is invalid.',
+                        1535803185
+                    );
+                }
+            }
+            if (isset($configuration['after'])) {
+                if (is_string($configuration['after'])) {
+                    $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true);
+                } elseif (!is_array($configuration['after'])) {
+                    throw new \UnexpectedValueException(
+                        'The specified "after" order configuration for provider "' . $provider . '" is invalid.',
+                        1535803186
+                    );
+                }
+            }
+        }
+        return $orderInformation;
+    }
+}
diff --git a/typo3/sysext/core/Classes/PageTitle/RecordPageTitleProvider.php b/typo3/sysext/core/Classes/PageTitle/RecordPageTitleProvider.php
new file mode 100644 (file)
index 0000000..67fcba9
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\PageTitle;
+
+/*
+ * 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!
+ */
+
+/**
+ * This class will take care of the default page title
+ */
+class RecordPageTitleProvider extends AbstractPageTitleProvider
+{
+    public function __construct()
+    {
+        $this->title = $GLOBALS['TSFE']->page['title'];
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-ConfigTitleTagFunction.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-ConfigTitleTagFunction.rst
new file mode 100644 (file)
index 0000000..1eef2b5
--- /dev/null
@@ -0,0 +1,32 @@
+.. include:: ../../Includes.txt
+
+=============================================
+Deprecation: #85678 - config.titleTagFunction
+=============================================
+
+See :issue:`85678`
+
+Description
+===========
+
+The TypoScript option :ts:`config.titleTagFunction` has been marked as deprecated and will be removed with TYPO3 v10.
+
+
+Impact
+======
+
+Installations using the option will trigger a PHP :php:`E_USER_DEPRECATED` error.
+
+
+Affected Installations
+======================
+
+Instances using the option.
+
+
+Migration
+=========
+
+Please use the new TitleTag API to alter the title tag.
+
+.. index:: TypoScript, NotScanned
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-TsfeAltPageTitle.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85678-TsfeAltPageTitle.rst
new file mode 100644 (file)
index 0000000..db1fab2
--- /dev/null
@@ -0,0 +1,32 @@
+.. include:: ../../Includes.txt
+
+====================================================
+Deprecation: #85678 - $GLOBALS['TSFE']->altPageTitle
+====================================================
+
+See :issue:`85678`
+
+Description
+===========
+
+The PHP property :php:`$GLOBALS['TSFE']->altPageTitle` has been marked as deprecated and will be removed with TYPO3 v10.
+
+
+Impact
+======
+
+Installations using this property will trigger a PHP :php:`E_USER_DEPRECATED` error.
+
+
+Affected Installations
+======================
+
+Instances using the property.
+
+
+Migration
+=========
+
+Please use the new TitleTag API to alter the title tag.
+
+.. index:: TypoScript, NotScanned
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-85678-AddAPIForTitleTags.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-85678-AddAPIForTitleTags.rst
new file mode 100644 (file)
index 0000000..d946142
--- /dev/null
@@ -0,0 +1,96 @@
+.. include:: ../../Includes.txt
+
+===================================
+Feature: #85678 - Add PageTitle API
+===================================
+
+See :issue:`85678`
+
+Description
+===========
+
+In order to keep setting the titles in control, a new API to set the page title has been introduced.
+
+The API uses :php:`PageTitleProviders` to define the page title based on page record and the content on the page.
+
+Based on the priority of the providers, the :php:`PageTitleProviderManager` will check the providers if a title
+is given by the provider. It will start with the highest priority PageTitleProviders and will end with the lowest
+in priority.
+
+By default, the core ships three providers. The provider with the (by default) highest priority will be the
+:php:`AltPageTitleProvider`. This provider handles the (since v9 deprecated) property
+:php:`$GLOBALS['TSFE']->altPageTitle`. If an extension has set a value to this property, this provider will return
+that value.
+
+If you have installed the system extension SEO, the second provider will be the :php:`SeoTitlePageTitleProvider`.
+When an editor has set a value for the SEO title in the page properties of the page, this provider will provide
+that title to the :php:`PageTitleProviderManager`. If you have not installed the SEO system extension, this fields
+and provider are not available.
+
+The fallback provider with the lowest priority is the :php:`RecordPageTitleProvider`. When no other title is set
+by a provider, this provider will return the title of the page.
+
+Besides the providers shipped by core, you can add own providers. An integrator can define the priority of the
+providers for his project.
+
+Create your own PageTitleProvider
+=================================
+
+Extension developer may want to have an own provider for page titles. For example if you have an extension with
+records and a detail view. The title of the page record, will not be the correct title. To make sure to display
+the correct page title, you have to create your own :php:`PageTitleProvider`. It is quite easy to create one.
+
+First of all create a PHP class in your extension that implements the :php:`PageTitleProviderInterface`. This will
+force you to have at least the :php:`getTitle()` method in your class. Within this method you can create your
+own logic to define the correct title.
+
+Define priority of PageTitleProviders
+=====================================
+
+The priority of the providers are set by the TypoScript property :typoscript:`config.pageTitleProviders`. This
+way an integrator is able to set the priorities for his project and can even have conditions in place.
+
+By default, the core has the following setup:
+
+.. code-block:: typoscript
+
+   config.pageTitleProviders {
+        altPageTitle {
+            provider = TYPO3\CMS\Core\PageTitle\AltPageTitleProvider
+            before = record
+        }
+        record {
+            provider = TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider
+        }
+   }
+
+The ordering of the providers is based on the `before` and `after` parameters. If you want a provider to be handled
+before a specific other provider, just set that provider in the `before`, do the same with `after`.
+
+If you have installed the system extension SEO, you will also get a third provider. The configuration will be:
+
+.. code-block:: typoscript
+
+   config.pageTitleProviders {
+      altPageTitle {
+         provider = TYPO3\CMS\Core\PageTitle\AltPageTitleProvider
+         before = record
+      }
+      record {
+         provider = TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider
+      }
+      seo {
+         provider = TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider
+         before = record
+         after = altPageTitle
+      }
+   }
+
+First the :php:`AltPageTitleProvider` will be checked, then the :php:`SeoTitlePageTitleProvider` (because it will be
+handled before record and after altPageTitle) and if both providers didn't provide a title, the
+:php:`RecordPageTitleProvider` will be checked.
+
+You can override these settings within your own installation. You can add as many providers as you want. Be aware
+that if a provider returns a non-empty value, all provider with a lower priority won't be checked.
+
+.. index:: Frontend, ext:core, ext:seo
index 728b989..d441fc7 100644 (file)
@@ -139,3 +139,16 @@ $metaTagManagerRegistry->registerManager(
     \TYPO3\CMS\Core\MetaTag\EdgeMetaTagManager::class
 );
 unset($metaTagManagerRegistry);
+
+// Add module configuration
+\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(trim('
+    config.pageTitleProviders {
+        altPageTitle {
+            provider = TYPO3\CMS\Core\PageTitle\AltPageTitleProvider
+            before = record
+        }
+        record {
+            provider = TYPO3\CMS\Core\PageTitle\RecordPageTitleProvider
+        }
+    }
+'));
index da57ef4..ded0d71 100644 (file)
@@ -49,6 +49,7 @@ use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
 use TYPO3\CMS\Core\Resource\StorageRepository;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Site\Entity\Site;
@@ -64,6 +65,7 @@ use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
@@ -3605,10 +3607,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             }
         }
 
-        $pageTitle = $this->altPageTitle ?: $this->page['title'] ?? '';
-        if (isset($this->page['seo_title']) && !empty($this->page['seo_title'])) {
-            $pageTitle = $this->page['seo_title'];
-        }
+        $titleProvider = GeneralUtility::makeInstance(ObjectManager::class)->get(PageTitleProviderManager::class);
+        $pageTitle = $titleProvider->getTitle();
 
         $titleTagContent = $this->printTitle(
             $pageTitle,
@@ -3617,6 +3617,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $pageTitleSeparator
         );
         if ($this->config['config']['titleTagFunction'] ?? false) {
+            // @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0
+            $this->logDeprecatedTyposcript('config.titleTagFunction', 'Please use the new TitleTag API to create custom title tags. Deprecated in version 9, will be removed in version 10');
+
             $titleTagContent = $this->cObj->callUserFunction(
                 $this->config['config']['titleTagFunction'],
                 [],
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/pages-title-tag.xml b/typo3/sysext/frontend/Tests/Functional/Fixtures/pages-title-tag.xml
new file mode 100644 (file)
index 0000000..8262f8f
--- /dev/null
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <pages>
+        <uid>1</uid>
+        <pid>0</pid>
+        <title>Root 1</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>2</uid>
+        <pid>1</pid>
+        <title>Dummy 1-2</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>3</uid>
+        <pid>1</pid>
+        <title>Dummy 1-3</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>4</uid>
+        <pid>1</pid>
+        <title>Dummy 1-4</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>5</uid>
+        <pid>2</pid>
+        <title>Dummy 1-2-5</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>6</uid>
+        <pid>2</pid>
+        <title>Dummy 1-2-6</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>7</uid>
+        <pid>2</pid>
+        <title>Dummy 1-2-7</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>8</uid>
+        <pid>3</pid>
+        <title>Dummy 1-3-8</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>9</uid>
+        <pid>3</pid>
+        <title>Dummy 1-3-9</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>10</uid>
+        <pid>4</pid>
+        <title>Dummy 1-4-10</title>
+        <deleted>0</deleted>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>11</uid>
+        <pid>0</pid>
+        <title>Workspace Root</title>
+        <deleted>0</deleted>
+        <t3ver_oid>0</t3ver_oid>
+        <t3ver_id>0</t3ver_id>
+        <t3ver_wsid>987654321</t3ver_wsid>
+        <t3ver_label>INITIAL PLACEHOLDER</t3ver_label>
+        <t3ver_state>1</t3ver_state>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+    <pages>
+        <uid>12</uid>
+        <pid>-1</pid>
+        <title>Workspace Root</title>
+        <deleted>0</deleted>
+        <t3ver_oid>11</t3ver_oid>
+        <t3ver_id>1</t3ver_id>
+        <t3ver_wsid>987654321</t3ver_wsid>
+        <t3ver_label>First draft version</t3ver_label>
+        <t3ver_state>-1</t3ver_state>
+        <perms_everybody>15</perms_everybody>
+    </pages>
+
+    <pages>
+        <uid>901</uid>
+        <pid>0</pid>
+        <l10n_parent>1</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Wurzel 1</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>902</uid>
+        <pid>1</pid>
+        <l10n_parent>2</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Attrappe 1-2</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>903</uid>
+        <pid>1</pid>
+        <l10n_parent>3</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Attrappe 1-3</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>904</uid>
+        <pid>2</pid>
+        <l10n_parent>5</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Attrappe 1-2-5</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>905</uid>
+        <pid>2</pid>
+        <l10n_parent>6</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Attrappe 1-2-6</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>906</uid>
+        <pid>3</pid>
+        <l10n_parent>9</l10n_parent>
+        <sys_language_uid>1</sys_language_uid>
+        <title>Attrappe 1-3-9</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>1000</uid>
+        <pid>1</pid>
+        <title>Root 1000</title>
+        <deleted>0</deleted>
+    </pages>
+    <pages>
+        <uid>1001</uid>
+        <pid>1</pid>
+        <title>Root 1001</title>
+        <seo_title>SEO Root 1001</seo_title>
+        <deleted>0</deleted>
+    </pages>
+</dataset>
+
diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/TitleTagRenderingTest.typoscript b/typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/TitleTagRenderingTest.typoscript
new file mode 100644 (file)
index 0000000..cc80b96
--- /dev/null
@@ -0,0 +1,18 @@
+page = PAGE
+page.typeNum = 0
+
+[globalString = GP:noPageTitle = 1]
+  config.noPageTitle = 1
+[globalString = GP:noPageTitle = 2]
+  config.noPageTitle = 2
+[end]
+
+[globalString = GP:headerData = 1]
+  page.headerData.100 = TEXT
+  page.headerData.100.value = Header Data Title
+  page.headerData.100.wrap = <title>|</title>
+[end]
+
+[globalString = GP:pageTitleTS = 1]
+  config.pageTitle.case = upper
+[end]
diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/TitleTagRenderingTest.php b/typo3/sysext/frontend/Tests/Functional/Rendering/TitleTagRenderingTest.php
new file mode 100644 (file)
index 0000000..4a090e8
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Frontend\Tests\Functional\Rendering;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Test case
+ */
+class TitleTagRenderingTest extends FunctionalTestCase
+{
+    /**
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = [
+        'core', 'frontend', 'seo'
+    ];
+
+    /**
+     * @var string[]
+     */
+    protected $pathsToLinkInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/AdditionalConfiguration.php' => 'typo3conf/AdditionalConfiguration.php',
+    ];
+
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->importDataSet('EXT:frontend/Tests/Functional/Fixtures/pages-title-tag.xml');
+        $this->setUpFrontendRootPage(
+            1,
+            ['EXT:frontend/Tests/Functional/Rendering/Fixtures/TitleTagRenderingTest.typoscript']
+        );
+        $this->setSiteTitleToTemplateRecord(
+            1,
+            'Site Title'
+        );
+    }
+
+    public function titleTagDataProvider(): array
+    {
+        return [
+            [
+                [
+                    'pageId' => 1000,
+                ],
+                [
+                    'assertRegExp' => '#<title>Site Title: Root 1000</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1001,
+                ],
+                [
+                    'assertRegExp' => '#<title>Site Title: SEO Root 1001</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1000,
+                    'noPageTitle' => 1,
+                ],
+                [
+                    'assertRegExp' => '#<title>Site Title</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1001,
+                    'noPageTitle' => 1,
+                ],
+                [
+                    'assertRegExp' => '#<title>Site Title</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1000,
+                    'noPageTitle' => 2,
+                ],
+                [
+                    'assertRegExp' => '',
+                    'assertNotRegExp' => '#<title>.*</title>#',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1001,
+                    'noPageTitle' => 2,
+                ],
+                [
+                    'assertRegExp' => '',
+                    'assertNotRegExp' => '#<title>.*</title>#',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1000,
+                    'noPageTitle' => 2,
+                    'headerData' => 1
+                ],
+                [
+                    'assertRegExp' => '#<title>Header Data Title</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1001,
+                    'noPageTitle' => 2,
+                    'headerData' => 1
+                ],
+                [
+                    'assertRegExp' => '#<title>Header Data Title</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+            [
+                [
+                    'pageId' => 1000,
+                    'pageTitleTS' => 1
+                ],
+                [
+                    'assertRegExp' => '#<title>SITE TITLE: ROOT 1000</title>#',
+                    'assertNotRegExp' => '',
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @param $pageConfig
+     * @param $expectations
+     * @test
+     * @dataProvider titleTagDataProvider
+     */
+    public function checkIfCorrectTitleTagIsRendered($pageConfig, $expectations): void
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())->withQueryParameters([
+                'id' => (int)$pageConfig['pageId'],
+                'noPageTitle' => (int)$pageConfig['noPageTitle'],
+                'headerData' => (int)$pageConfig['headerData'],
+                'pageTitleTS' => (int)$pageConfig['pageTitleTS']
+            ])
+        );
+        $content = (string)$response->getBody();
+        if ($expectations['assertRegExp']) {
+            $this->assertRegExp($expectations['assertRegExp'], $content);
+        }
+        if ($expectations['assertNotRegExp']) {
+            $this->assertNotRegExp($expectations['assertNotRegExp'], $content);
+        }
+    }
+
+    /**
+     * Adds site title to template record
+     *
+     * @param int $pageId
+     * @param string $siteTitle
+     */
+    protected function setSiteTitleToTemplateRecord(int $pageId, string $siteTitle): void
+    {
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_template');
+
+        $template = $connection->select(['uid', 'sitetitle'], 'sys_template', ['pid' => $pageId, 'root' => 1])->fetch();
+        if (empty($template)) {
+            $this->fail('Cannot find root template on page with id: "' . $pageId . '"');
+        }
+        $updateFields['sitetitle'] = $siteTitle;
+        $connection->update(
+            'sys_template',
+            $updateFields,
+            ['uid' => $template['uid']]
+        );
+    }
+}
diff --git a/typo3/sysext/seo/Classes/PageTitle/SeoTitlePageTitleProvider.php b/typo3/sysext/seo/Classes/PageTitle/SeoTitlePageTitleProvider.php
new file mode 100644 (file)
index 0000000..77e469e
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Seo\PageTitle;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
+
+/**
+ * This class will take care of the seo title that can be set in the backend
+ */
+class SeoTitlePageTitleProvider extends AbstractPageTitleProvider
+{
+    public function __construct()
+    {
+        $this->title = (string)$GLOBALS['TSFE']->page['seo_title'];
+    }
+}
index 6128049..3e0c9ec 100644 (file)
@@ -14,3 +14,14 @@ $metaTagManagerRegistry->registerManager(
     \TYPO3\CMS\Seo\MetaTag\TwitterCardMetaTagManager::class
 );
 unset($metaTagManagerRegistry);
+
+// Add module configuration
+\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(trim('
+    config.pageTitleProviders {
+        seo {
+           provider = TYPO3\CMS\Seo\PageTitle\SeoTitlePageTitleProvider
+            before = record
+            after = altPageTitle
+        }
+    }
+'));