diff --git a/Classes/Event/Listener/AfterCacheableContentIsGeneratedListener.php b/Classes/Event/Listener/AfterCacheableContentIsGeneratedListener.php index 7150ec62..5d547800 100644 --- a/Classes/Event/Listener/AfterCacheableContentIsGeneratedListener.php +++ b/Classes/Event/Listener/AfterCacheableContentIsGeneratedListener.php @@ -12,30 +12,139 @@ namespace FriendsOfTYPO3\Headless\Event\Listener; use FriendsOfTYPO3\Headless\Json\JsonEncoder; +use FriendsOfTYPO3\Headless\Utility\HeadlessMode; +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry; +use TYPO3\CMS\Core\TypoScript\TypoScriptService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; +use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent; +use function array_merge; use function json_decode; use const JSON_THROW_ON_ERROR; class AfterCacheableContentIsGeneratedListener { - public function __construct(private readonly JsonEncoder $encoder) {} + public function __construct( + private readonly JsonEncoder $encoder, + private readonly MetaTagManagerRegistry $metaTagRegistry, + private readonly EventDispatcherInterface $eventDispatcher, + ) {} public function __invoke(AfterCacheableContentIsGeneratedEvent $event) { try { + if (!GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($event->getRequest())->isEnabled()) { + return; + } + $content = json_decode($event->getController()->content, true, 512, JSON_THROW_ON_ERROR); - if (($content['meta']['title'] ?? null) === null) { + if (($content['seo']['title'] ?? null) === null) { return; } - $content['meta']['title'] = $event->getController()->generatePageTitle(); + $_params = ['page' => $event->getController()->page, 'request' => $event->getRequest(), '_seoLinks' => []]; + $_ref = null; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) { + GeneralUtility::callUserFunction($_funcRef, $_params, $_ref); + } + + $content['seo']['title'] = $event->getController()->generatePageTitle(); + + $this->generateMetaTagsFromTyposcript($event->getController()->pSetup['meta.'] ?? [], $event->getController()->cObj); + + $metaTags = []; + $metaTagManagers = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getAllManagers(); + + foreach ($metaTagManagers as $manager => $managerObject) { + $properties = json_decode($managerObject->renderAllProperties(), true); + if (!empty($properties)) { + $metaTags = array_merge($metaTags, $properties); + } + } + + $content['seo']['meta'] = $metaTags; + + $hrefLangs = $this->eventDispatcher->dispatch(new ModifyHrefLangTagsEvent($event->getRequest()))->getHrefLangs(); + + $seoLinks = $_params['_seoLinks'] ?? []; + + if (count($hrefLangs) > 1) { + foreach ($hrefLangs as $hrefLang => $href) { + $seoLinks[] = ['rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href]; + } + } + + if ($seoLinks !== []) { + $content['seo']['link'] = $seoLinks; + } $event->getController()->content = $this->encoder->encode($content); - } catch (\Throwable) { + } catch (\Throwable $e) { return; } } + + /** + * @codeCoverageIgnore + */ + protected function generateMetaTagsFromTyposcript(array $metaTagTypoScript, ContentObjectRenderer $cObj) + { + $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class); + $conf = $typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript); + foreach ($conf as $key => $properties) { + $replace = false; + if (is_array($properties)) { + $nodeValue = $properties['_typoScriptNodeValue'] ?? ''; + $value = trim((string)$cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.'])); + if ($value === '' && !empty($properties['value'])) { + $value = $properties['value']; + $replace = false; + } + } else { + $value = $properties; + } + + $attribute = 'name'; + if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') { + $attribute = 'http-equiv'; + } + if (is_array($properties) && !empty($properties['attribute'])) { + $attribute = $properties['attribute']; + } + if (is_array($properties) && !empty($properties['replace'])) { + $replace = true; + } + + if (!is_array($value)) { + $value = (array)$value; + } + foreach ($value as $subValue) { + if (trim($subValue ?? '') !== '') { + $this->setMetaTag($attribute, $key, $subValue, [], $replace); + } + } + } + } + + /** + * @codeCoverageIgnore + */ + private function setMetaTag(string $type, string $name, string $content, array $subProperties = [], $replace = true): void + { + $type = strtolower($type); + $name = strtolower($name); + if (!in_array($type, ['property', 'name', 'http-equiv'], true)) { + throw new \InvalidArgumentException( + 'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.', + 1496402460 + ); + } + $manager = $this->metaTagRegistry->getManagerForProperty($name); + $manager->addProperty($name, $content, $subProperties, $replace, $type); + } } diff --git a/Classes/Seo/CanonicalGenerator.php b/Classes/Seo/CanonicalGenerator.php new file mode 100644 index 00000000..fafb93cc --- /dev/null +++ b/Classes/Seo/CanonicalGenerator.php @@ -0,0 +1,84 @@ +typoScriptFrontendController->config['config']['disableCanonical'] ?? false) { + return ''; + } + + $event = new ModifyUrlForCanonicalTagEvent('', $params['request'], new Page($params['page'])); + $event = $this->eventDispatcher->dispatch($event); + $href = $event->getUrl(); + + if (empty($href) && (int)$this->typoScriptFrontendController->page['no_index'] === 1) { + return ''; + } + + if (empty($href)) { + // 1) Check if page has canonical URL set + $href = $this->checkForCanonicalLink(); + } + if (empty($href)) { + // 2) Check if page show content from other page + $href = $this->checkContentFromPid(); + } + if (empty($href)) { + // 3) Fallback, create canonical URL + $href = $this->checkDefaultCanonical(); + } + + if (!empty($href)) { + if (GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($params['request'])->isEnabled()) { + $canonical = [ + 'href' => htmlspecialchars($href), + 'rel' => 'canonical', + ]; + + $params['_seoLinks'][] = $canonical; + $canonical = json_encode($canonical); + } else { + $canonical = ' 'canonical', + 'href' => $href, + ], true) . '/>' . LF; + $this->typoScriptFrontendController->additionalHeaderData[] = $canonical; + } + + return $canonical; + } + return ''; + } +} diff --git a/Classes/Seo/MetaTag/AbstractMetaTagManager.php b/Classes/Seo/MetaTag/AbstractMetaTagManager.php new file mode 100644 index 00000000..1f49cf1c --- /dev/null +++ b/Classes/Seo/MetaTag/AbstractMetaTagManager.php @@ -0,0 +1,102 @@ +withRequest($GLOBALS['TYPO3_REQUEST'])->isEnabled()) { + return $this->renderAllHeadlessProperties(); + } + + return parent::renderAllProperties(); + } + + public function renderProperty(string $property): string + { + if (GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($GLOBALS['TYPO3_REQUEST'])->isEnabled()) { + return $this->renderHeadlessProperty($property); + } + + return parent::renderProperty($property); + } + + /** + * Render a meta tag for a specific property + * + * @param string $property Name of the property + */ + public function renderHeadlessProperty(string $property): string + { + $property = strtolower($property); + $metaTags = []; + + $nameAttribute = $this->defaultNameAttribute; + if (isset($this->handledProperties[$property]['nameAttribute']) + && !empty((string)$this->handledProperties[$property]['nameAttribute'])) { + $nameAttribute = (string)$this->handledProperties[$property]['nameAttribute']; + } + + $contentAttribute = $this->defaultContentAttribute; + if (isset($this->handledProperties[$property]['contentAttribute']) + && !empty((string)$this->handledProperties[$property]['contentAttribute'])) { + $contentAttribute = (string)$this->handledProperties[$property]['contentAttribute']; + } + + if ($nameAttribute && $contentAttribute) { + foreach ($this->getProperty($property) as $propertyItem) { + $metaTags[] = [ + htmlspecialchars($nameAttribute) => htmlspecialchars($property), + htmlspecialchars($contentAttribute) => htmlspecialchars($propertyItem['content']), + ]; + + if (!count($propertyItem['subProperties'])) { + continue; + } + foreach ($propertyItem['subProperties'] as $subProperty => $subPropertyItems) { + foreach ($subPropertyItems as $subPropertyItem) { + $metaTags[] = [ + htmlspecialchars($nameAttribute) => htmlspecialchars($property . $this->subPropertySeparator . $subProperty), + htmlspecialchars($contentAttribute) => htmlspecialchars((string)$subPropertyItem), + ]; + } + } + } + } + + return json_encode($metaTags); + } + + /** + * Render all registered properties of this manager + */ + public function renderAllHeadlessProperties(): string + { + $metatags = []; + foreach (array_keys($this->properties) as $property) { + $metatags = array_merge($metatags, json_decode($this->renderHeadlessProperty($property), true)); + } + + return json_encode($metatags); + } +} diff --git a/Classes/Seo/MetaTag/EdgeMetaTagManager.php b/Classes/Seo/MetaTag/EdgeMetaTagManager.php new file mode 100644 index 00000000..8f4e3bf2 --- /dev/null +++ b/Classes/Seo/MetaTag/EdgeMetaTagManager.php @@ -0,0 +1,25 @@ + ['nameAttribute' => 'http-equiv'], + ]; +} diff --git a/Classes/Seo/MetaTag/Html5MetaTagManager.php b/Classes/Seo/MetaTag/Html5MetaTagManager.php new file mode 100644 index 00000000..69cc8f78 --- /dev/null +++ b/Classes/Seo/MetaTag/Html5MetaTagManager.php @@ -0,0 +1,61 @@ + [], + 'author' => [], + 'description' => [], + 'generator' => [], + 'keywords' => [], + 'referrer' => [], + 'content-language' => [ + 'nameAttribute' => 'http-equiv', + ], + 'content-type' => [ + 'nameAttribute' => 'http-equiv', + ], + 'default-style' => [ + 'nameAttribute' => 'http-equiv', + ], + 'refresh' => [ + 'nameAttribute' => 'http-equiv', + ], + 'set-cookie' => [ + 'nameAttribute' => 'http-equiv', + ], + 'content-security-policy' => [ + 'nameAttribute' => 'http-equiv', + ], + 'viewport' => [], + 'robots' => [], + 'expires' => [ + 'nameAttribute' => 'http-equiv', + ], + 'cache-control' => [ + 'nameAttribute' => 'http-equiv', + ], + 'pragma' => [ + 'nameAttribute' => 'http-equiv', + ], + ]; +} diff --git a/Classes/Seo/MetaTag/OpenGraphMetaTagManager.php b/Classes/Seo/MetaTag/OpenGraphMetaTagManager.php new file mode 100644 index 00000000..1c59a35f --- /dev/null +++ b/Classes/Seo/MetaTag/OpenGraphMetaTagManager.php @@ -0,0 +1,61 @@ + by default + * + * @var string + */ + protected $defaultNameAttribute = 'property'; + + /** + * Array of properties that can be handled by this manager + * + * @var array + */ + protected $handledProperties = [ + 'og:type' => [], + 'og:title' => [], + 'og:description' => [], + 'og:site_name' => [], + 'og:url' => [], + 'og:audio' => [], + 'og:video' => [], + 'og:determiner' => [], + 'og:locale' => [ + 'allowedSubProperties' => [ + 'alternate' => [ + 'allowMultipleOccurrences' => true, + ], + ], + ], + 'og:image' => [ + 'allowMultipleOccurrences' => true, + 'allowedSubProperties' => [ + 'url' => [], + 'secure_url' => [], + 'type' => [], + 'width' => [], + 'height' => [], + 'alt' => [], + ], + ], + ]; +} diff --git a/Classes/Seo/MetaTag/TwitterCardMetaTagManager.php b/Classes/Seo/MetaTag/TwitterCardMetaTagManager.php new file mode 100644 index 00000000..baf486a7 --- /dev/null +++ b/Classes/Seo/MetaTag/TwitterCardMetaTagManager.php @@ -0,0 +1,64 @@ + [], + 'twitter:site' => [ + 'allowedSubProperties' => [ + 'id' => [], + ], + ], + 'twitter:creator' => [ + 'allowedSubProperties' => [ + 'id' => [], + ], + ], + 'twitter:description' => [], + 'twitter:title' => [], + 'twitter:image' => [ + 'allowedSubProperties' => [ + 'alt' => [], + ], + ], + 'twitter:player' => [ + 'allowedSubProperties' => [ + 'width' => [], + 'height' => [], + 'stream' => [], + ], + ], + 'twitter:app' => [ + 'allowedSubProperties' => [ + 'name:iphone' => [], + 'id:iphone' => [], + 'url:iphone' => [], + 'name:ipad' => [], + 'id:ipad' => [], + 'url:ipad' => [], + 'name:googleplay' => [], + 'id:googleplay' => [], + 'url:googleplay' => [], + ], + ], + ]; +} diff --git a/Configuration/Services.php b/Configuration/Services.php index 516fd31c..d6dddabb 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -79,15 +79,10 @@ 'event.listener', ['identifier' => 'headless/AfterLinkIsGenerated'] ); - - $features = GeneralUtility::makeInstance(Features::class); - - if ($features->isFeatureEnabled('headless.pageTitleProviders')) { - $services->set(AfterCacheableContentIsGeneratedListener::class)->tag( - 'event.listener', - ['identifier' => 'headless/AfterCacheableContentIsGenerated'] - ); - } + $services->set(AfterCacheableContentIsGeneratedListener::class)->tag( + 'event.listener', + ['identifier' => 'headless/AfterCacheableContentIsGenerated'] + ); if ($feloginInstalled) { $services->set(LoginConfirmedEventListener::class)->tag( @@ -105,6 +100,7 @@ $services->set(FormTranslationService::class)->arg('$runtimeCache', service('cache.runtime'))->public(); } + $features = GeneralUtility::makeInstance(Features::class); if ($features->isFeatureEnabled('headless.overrideFluidTemplates')) { $templateService = $services->alias( \TYPO3\CMS\Fluid\View\TemplateView::class, diff --git a/Configuration/TypoScript/Configuration/PageConfiguration.typoscript b/Configuration/TypoScript/Configuration/PageConfiguration.typoscript index 3a1c81f0..0441714b 100644 --- a/Configuration/TypoScript/Configuration/PageConfiguration.typoscript +++ b/Configuration/TypoScript/Configuration/PageConfiguration.typoscript @@ -1,6 +1,11 @@ page < lib.headlessPage page { typeNum = 0 + meta { + keywords.data = page:keywords + generator = TYPO3 CMS x T3Headless + generator.replace = 1 + } 10 { fields { @@ -8,6 +13,7 @@ page { id { field = uid } + type = CASE type { key.field = doktype @@ -36,10 +42,12 @@ page { default = TEXT default.value = Standard } + slug = TEXT slug { field = slug } + media = TEXT media { dataProcessing { @@ -50,6 +58,19 @@ page { } } } + + # placeholder only, overridden later on via event + seo = JSON + seo { + fields { + title = TEXT + title { + field = title + } + } + } + + # backwards compatibility legacy handling of meta tags, to be removed in future major releases meta =< lib.meta categories =< lib.categories breadcrumbs =< lib.breadcrumbs diff --git a/Configuration/TypoScript/Page/Meta.typoscript b/Configuration/TypoScript/Page/Meta.typoscript index b07d9e59..c2159921 100644 --- a/Configuration/TypoScript/Page/Meta.typoscript +++ b/Configuration/TypoScript/Page/Meta.typoscript @@ -1,3 +1,4 @@ +# legacy meta handling, to be removed in future major releases. lib.meta = JSON lib.meta { fields { diff --git a/Tests/Unit/Event/Listener/AfterCacheableContentIsGeneratedListenerTest.php b/Tests/Unit/Event/Listener/AfterCacheableContentIsGeneratedListenerTest.php index cd189b0a..6280b763 100644 --- a/Tests/Unit/Event/Listener/AfterCacheableContentIsGeneratedListenerTest.php +++ b/Tests/Unit/Event/Listener/AfterCacheableContentIsGeneratedListenerTest.php @@ -13,10 +13,22 @@ use FriendsOfTYPO3\Headless\Event\Listener\AfterCacheableContentIsGeneratedListener; use FriendsOfTYPO3\Headless\Json\JsonEncoder; +use FriendsOfTYPO3\Headless\Seo\MetaTag\Html5MetaTagManager; +use FriendsOfTYPO3\Headless\Utility\Headless; +use FriendsOfTYPO3\Headless\Utility\HeadlessMode; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; +use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; +use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; use function json_encode; @@ -24,12 +36,15 @@ class AfterCacheableContentIsGeneratedListenerTest extends UnitTestCase { use ProphecyTrait; + protected bool $resetSingletonInstances = true; public function testNotModifiedWithInvalidJsonContent(): void { - $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder()); + $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder(), $this->prophesize(MetaTagManagerRegistry::class)->reveal(), $this->prophesize(EventDispatcherInterface::class)->reveal()); $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute(Argument::is('headless'))->willReturn(new Headless(HeadlessMode::FULL)); + $controller = $this->prophesize(TypoScriptFrontendController::class); $controller->content = ''; @@ -42,11 +57,13 @@ public function testNotModifiedWithInvalidJsonContent(): void public function testNotModifiedWhileValidJson(): void { - $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder()); + $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder(), $this->prophesize(MetaTagManagerRegistry::class)->reveal(), $this->prophesize(EventDispatcherInterface::class)->reveal()); $content = json_encode(['someCustomPageWithoutMeta' => ['title' => 'test before event']]); $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute(Argument::is('headless'))->willReturn(new Headless(HeadlessMode::FULL)); + $controller = $this->prophesize(TypoScriptFrontendController::class); $controller->content = $content; $controller->generatePageTitle()->willReturn('Modified title via PageTitleManager'); @@ -60,17 +77,69 @@ public function testNotModifiedWhileValidJson(): void public function testModifiedPageTitle(): void { - $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder()); + $listenerProvider = $this->prophesize(ListenerProvider::class); + $listenerProvider->getListenersForEvent(Argument::any())->willReturn([]); + + $eventDispatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Core\EventDispatcher\EventDispatcher::class, $listenerProvider->reveal()); + + $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder(), $this->prophesize(MetaTagManagerRegistry::class)->reveal(), $eventDispatcher); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute(Argument::is('headless'))->willReturn(new Headless(HeadlessMode::FULL)); + $controller = $this->prophesize(TypoScriptFrontendController::class); + $controller->content = json_encode(['meta' => ['title' => 'test before event'], 'seo' => ['title' => 'test before event']]); + $controller->cObj = $this->prophesize(ContentObjectRenderer::class)->reveal(); + $controller->generatePageTitle()->willReturn('Modified title via PageTitleProviderManager'); + + $event = new AfterCacheableContentIsGeneratedEvent($request->reveal(), $controller->reveal(), 'abc', false); + + $listener($event); + + self::assertSame(json_encode(['meta' => ['title' => 'test before event'], 'seo' => ['title' => 'Modified title via PageTitleProviderManager', 'meta' => []]]), $event->getController()->content); + } + + public function testHreflangs(): void + { + $event = new ModifyHrefLangTagsEvent(new ServerRequest()); + $event->setHrefLangs([ + 'pl-PL' => 'https://example.com/pl', + 'en-US' => 'https://example.com/us', + 'en-UK' => 'https://example.com/uk', + ]); + + $eventDispatcher = $this->prophesize(EventDispatcher::class); + $eventDispatcher->dispatch(Argument::any())->willReturn($event); + + $listener = new AfterCacheableContentIsGeneratedListener(new JsonEncoder(), $this->prophesize(MetaTagManagerRegistry::class)->reveal(), $eventDispatcher->reveal()); $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute(Argument::is('headless'))->willReturn(new Headless(HeadlessMode::FULL)); + $GLOBALS['TYPO3_REQUEST'] = $request->reveal(); $controller = $this->prophesize(TypoScriptFrontendController::class); - $controller->content = json_encode(['meta' => ['title' => 'test before event']]); + $controller->content = json_encode(['meta' => ['title' => 'test before event'], 'seo' => ['title' => 'test before event']]); + $controller->cObj = $this->prophesize(ContentObjectRenderer::class)->reveal(); $controller->generatePageTitle()->willReturn('Modified title via PageTitleProviderManager'); + $registry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class); + $registry->registerManager('html5', Html5MetaTagManager::class); + $manager = $registry->getManagerForProperty('generator'); + + $testHook = new class () { + public function handle(): void {} + }; + + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags']['test'] = $testHook::class . '->handle'; + + $manager->addProperty('generator', 'TYPO3 CMS x T3Headless', [], true, 'name'); + $event = new AfterCacheableContentIsGeneratedEvent($request->reveal(), $controller->reveal(), 'abc', false); $listener($event); - self::assertSame(json_encode(['meta' => ['title' => 'Modified title via PageTitleProviderManager']]), $event->getController()->content); + self::assertSame(json_encode(['meta' => ['title' => 'test before event'], 'seo' => ['title' => 'Modified title via PageTitleProviderManager', 'meta' => [['name' => 'generator', 'content' => 'TYPO3 CMS x T3Headless']], 'link' => [ + ['rel' => 'alternate', 'hreflang' => 'pl-PL', 'href' => 'https://example.com/pl'], + ['rel' => 'alternate', 'hreflang' => 'en-US', 'href' => 'https://example.com/us'], + ['rel' => 'alternate', 'hreflang' => 'en-UK', 'href' => 'https://example.com/uk'], + ]]]), $event->getController()->content); } } diff --git a/Tests/Unit/Seo/MetaTag/MetaTagTest.php b/Tests/Unit/Seo/MetaTag/MetaTagTest.php new file mode 100644 index 00000000..20bfd684 --- /dev/null +++ b/Tests/Unit/Seo/MetaTag/MetaTagTest.php @@ -0,0 +1,69 @@ +prophesize(PageRenderer::class); + $pageRenderer->getDocType()->willReturn(\TYPO3\CMS\Core\Type\DocType::html5); + + $container->set(PageRenderer::class, $pageRenderer->reveal()); + + GeneralUtility::setContainer($container); + + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()); + + $registry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class); + + $registry->registerManager( + 'html5', + Html5MetaTagManager::class + ); + + $registry->registerManager( + 'opengraph', + OpenGraphMetaTagManager::class + ); + + $htmlManager = $registry->getManagerForProperty('generator'); + $htmlManager->addProperty('generator', 'TYPO3 CMS x T3Headless', [], true, 'name'); + + $ogManager = $registry->getManagerForProperty('og:image'); + $ogManager->addProperty('og:image', 'Powered by TYPO3', ['url' => 'https://example.com/image.jpg'], true, 'name'); + + self::assertSame('', $htmlManager->renderAllProperties()); + + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('headless', new Headless(HeadlessMode::FULL)); + + self::assertSame('[{"name":"generator","content":"TYPO3 CMS x T3Headless"}]', $htmlManager->renderProperty('generator')); + self::assertSame('[{"name":"generator","content":"TYPO3 CMS x T3Headless"}]', $htmlManager->renderAllProperties()); + self::assertSame('[{"property":"og:image","content":"Powered by TYPO3"},{"property":"og:image:url","content":"https:\/\/example.com\/image.jpg"}]', $ogManager->renderAllProperties()); + } +} diff --git a/ext_localconf.php b/ext_localconf.php index b4806c04..407b0d7c 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -8,12 +8,18 @@ */ use FriendsOfTYPO3\Headless\Hooks\FileOrFolderLinkBuilder; +use FriendsOfTYPO3\Headless\Seo\MetaTag\EdgeMetaTagManager; +use FriendsOfTYPO3\Headless\Seo\MetaTag\Html5MetaTagManager; +use FriendsOfTYPO3\Headless\Seo\MetaTag\OpenGraphMetaTagManager; +use FriendsOfTYPO3\Headless\Seo\MetaTag\TwitterCardMetaTagManager; use FriendsOfTYPO3\Headless\Resource\Rendering\AudioTagRenderer; use FriendsOfTYPO3\Headless\Resource\Rendering\VideoTagRenderer; use FriendsOfTYPO3\Headless\Resource\Rendering\VimeoRenderer; use FriendsOfTYPO3\Headless\Resource\Rendering\YouTubeRenderer; +use FriendsOfTYPO3\Headless\Seo\CanonicalGenerator; use FriendsOfTYPO3\Headless\XClass\ResourceLocalDriver; use TYPO3\CMS\Core\Configuration\Features; +use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry; use TYPO3\CMS\Core\Resource\Driver\LocalDriver; use TYPO3\CMS\Core\Resource\Rendering\RendererRegistry; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; @@ -73,6 +79,28 @@ static function () { ]; } + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags']['canonical'] = + CanonicalGenerator::class . '->handle'; + + $metaTagManagerRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class); + $metaTagManagerRegistry->registerManager( + 'html5', + Html5MetaTagManager::class + ); + $metaTagManagerRegistry->registerManager( + 'edge', + EdgeMetaTagManager::class + ); + $metaTagManagerRegistry->registerManager( + 'opengraph', + OpenGraphMetaTagManager::class + ); + $metaTagManagerRegistry->registerManager( + 'twitter', + TwitterCardMetaTagManager::class + ); + unset($metaTagManagerRegistry); + $rendererRegistry = GeneralUtility::makeInstance(RendererRegistry::class); $rendererRegistry->registerRendererClass(YouTubeRenderer::class); $rendererRegistry->registerRendererClass(VimeoRenderer::class);