Skip to content

Commit

Permalink
[FEATURE] Allow usage of SEO APIs with USER_INT plugins (#774)
Browse files Browse the repository at this point in the history
Small refactoring allows to handle also dynamic (non cachable) plugins which utilize SEO related API (PageTitle, MetaTag)
  • Loading branch information
twoldanski committed Sep 18, 2024
1 parent 4c5fe0f commit 7e71a14
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 122 deletions.
112 changes: 10 additions & 102 deletions Classes/Event/Listener/AfterCacheableContentIsGeneratedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@
namespace FriendsOfTYPO3\Headless\Event\Listener;

use FriendsOfTYPO3\Headless\Json\JsonEncoder;
use FriendsOfTYPO3\Headless\Seo\MetaHandler;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt;
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;
Expand All @@ -30,8 +26,8 @@ class AfterCacheableContentIsGeneratedListener
{
public function __construct(
private readonly JsonEncoder $encoder,
private readonly MetaTagManagerRegistry $metaTagRegistry,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly MetaHandler $metaHandler,
private readonly HeadlessUserInt $headlessUserInt,
) {}

public function __invoke(AfterCacheableContentIsGeneratedEvent $event)
Expand All @@ -41,110 +37,22 @@ public function __invoke(AfterCacheableContentIsGeneratedEvent $event)
return;
}

$content = json_decode($event->getController()->content, true, 512, JSON_THROW_ON_ERROR);

if (($content['seo']['title'] ?? null) === null) {
if ($this->headlessUserInt->hasNonCacheableContent($event->getController()->content)) {
// we have dynamic content on page, we fire MetaHandler later on middleware
return;
}

$_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'] ?? [];
$content = json_decode($event->getController()->content, true, 512, JSON_THROW_ON_ERROR);

if (count($hrefLangs) > 1) {
foreach ($hrefLangs as $hrefLang => $href) {
$seoLinks[] = ['rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href];
}
if (($content['seo']['title'] ?? null) === null) {
return;
}

if ($seoLinks !== []) {
$content['seo']['link'] = $seoLinks;
}
$content = $this->metaHandler->process($event->getRequest(), $event->getController(), $content);

$event->getController()->content = $this->encoder->encode($content);
} 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);
}
}
38 changes: 25 additions & 13 deletions Classes/Middleware/UserIntMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,24 @@

namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Seo\MetaHandler;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Http\Stream;
use TYPO3\CMS\Core\Utility\GeneralUtility;

use function json_decode;

class UserIntMiddleware implements MiddlewareInterface
{
private HeadlessUserInt $headlessUserInt;
private HeadlessMode $headlessMode;

public function __construct(
HeadlessUserInt $headlessUserInt = null,
HeadlessMode $headlessMode
) {
$this->headlessUserInt = $headlessUserInt ?? GeneralUtility::makeInstance(HeadlessUserInt::class);
$this->headlessMode = $headlessMode;
}
private readonly HeadlessUserInt $headlessUserInt,
private readonly HeadlessMode $headlessMode,
private readonly MetaHandler $metaHandler
) {}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
Expand All @@ -41,11 +38,26 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
return $response;
}

$body = $this->headlessUserInt->unwrap($response->getBody()->__toString());
$jsonContent = $response->getBody()->__toString();

$stream = new Stream('php://temp', 'r+');
$stream->write($body);
if (!$this->headlessUserInt->hasNonCacheableContent($jsonContent)) {
return $response;
}

$jsonContent = $this->headlessUserInt->unwrap($jsonContent);
$responseBody = json_decode($jsonContent, true);

if (($responseBody['seo']['title'] ?? null) !== null) {
$responseBody = $this->metaHandler->process(
$request,
$request->getAttribute('frontend.controller'),
$responseBody
);
$jsonContent = json_encode($responseBody);
}

$stream = new Stream('php://temp', 'r+');
$stream->write($jsonContent);
return $response->withBody($stream);
}
}
132 changes: 132 additions & 0 deletions Classes/Seo/MetaHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

/*
* This file is part of the "headless" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

declare(strict_types=1);

namespace FriendsOfTYPO3\Headless\Seo;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
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\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

class MetaHandler
{
public function __construct(
private readonly MetaTagManagerRegistry $metaTagRegistry,
private readonly EventDispatcherInterface $eventDispatcher,
) {}

public function process(ServerRequestInterface $request, TypoScriptFrontendController $controller, array $content): array
{
$_params = ['page' => $controller->page, 'request' => $request, '_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'] = $controller->generatePageTitle();

$this->generateMetaTagsFromTyposcript(
$controller->pSetup['meta.'] ?? [],
$controller->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($request))->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;
}

return $content;
}

/**
* @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);
}
}
5 changes: 5 additions & 0 deletions Classes/Utility/HeadlessUserInt.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public function wrap(string $content, string $type = self::STANDARD): string
);
}

public function hasNonCacheableContent(string $content): bool
{
return str_contains($content, self::STANDARD);
}

public function unwrap(string $content): string
{
if (str_contains($content, self::NESTED)) {
Expand Down
Loading

0 comments on commit 7e71a14

Please sign in to comment.