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);