• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TYPO3-Headless / headless / 25562272739

08 May 2026 02:50PM UTC coverage: 74.7% (+1.7%) from 73.04%
25562272739

push

github

lukaszuznanski
Merge branch 'feature/typo3-v14'

38 of 58 new or added lines in 10 files covered. (65.52%)

23 existing lines in 2 files now uncovered.

1181 of 1581 relevant lines covered (74.7%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.66
/Classes/Seo/MetaHandler.php
1
<?php
2

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

10
declare(strict_types=1);
11

12
namespace FriendsOfTYPO3\Headless\Seo;
13

14
use InvalidArgumentException;
15
use Psr\EventDispatcher\EventDispatcherInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
18
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
19
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
20
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
23
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
24

25
use function array_merge;
26
use function array_merge_recursive;
27
use function htmlspecialchars;
28
use function implode;
29

30
class MetaHandler implements MetaHandlerInterface
31
{
32
    public function __construct(
33
        private readonly MetaTagManagerRegistry $metaTagRegistry,
34
        private readonly EventDispatcherInterface $eventDispatcher,
35
        private readonly PageTitleProviderManager $pageTitleProviderManager,
36
        private readonly TypoScriptService $typoScriptService,
37
    ) {}
31✔
38

39
    public function process(
40
        ServerRequestInterface $request,
41
        array $content
42
    ): array {
43
        $pageInformation = $request->getAttribute('frontend.page.information');
3✔
44
        $page = $pageInformation->getPageRecord();
3✔
45

46
        $_params = ['page' => $page, 'request' => $request, '_seoLinks' => []];
3✔
47
        $_ref = null;
3✔
48
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) {
3✔
UNCOV
49
            GeneralUtility::callUserFunction($_funcRef, $_params, $_ref);
×
50
        }
51

52
        $typoScriptSetup = $request->getAttribute('frontend.typoscript')->getSetupArray();
3✔
53
        $typoScriptConfig = $typoScriptSetup['config.'] ?? [];
3✔
54

55
        $content['seo']['title'] = $this->generatePageTitle($request, $typoScriptConfig);
3✔
56

57
        $cObj = $this->createContentObjectRenderer($request, $page);
3✔
58
        $this->generateMetaTagsFromTyposcript(
3✔
59
            $typoScriptSetup['page.']['meta.'] ?? [],
3✔
60
            $cObj
3✔
61
        );
3✔
62

63
        $metaTags = [];
3✔
64
        $metaTagManagers = $this->metaTagRegistry->getAllManagers();
3✔
65

66
        foreach ($metaTagManagers as $managerObject) {
3✔
67
            $properties = json_decode($managerObject->renderAllProperties(), true);
1✔
68
            if (!empty($properties)) {
1✔
69
                $metaTags = array_merge($metaTags, $properties);
1✔
70
            }
71
        }
72

73
        $content['seo']['meta'] = $metaTags;
3✔
74

75
        $hrefLangs = $this->eventDispatcher->dispatch(new ModifyHrefLangTagsEvent($request))->getHrefLangs();
3✔
76

77
        $seoLinks = $_params['_seoLinks'] ?? [];
3✔
78

79
        if (count($hrefLangs) > 1) {
3✔
80
            foreach ($hrefLangs as $hrefLang => $href) {
1✔
81
                $seoLinks[] = ['rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href];
1✔
82
            }
83
        }
84

85
        if ($seoLinks !== []) {
3✔
86
            $content['seo']['link'] = $seoLinks;
1✔
87
        }
88

89
        /**
90
         * @var SiteLanguage $language
91
         */
92
        $language = $request->getAttribute('language');
3✔
93

94
        $rawHtmlTagAttrs = $typoScriptConfig['htmlTag.']['attributes.'] ?? [];
3✔
95
        $overwriteBodyTag = (int)($typoScriptConfig['headless.']['overwriteBodyTag'] ?? 0);
3✔
96
        $htmlTagAttrs = $this->normalizeAttr($rawHtmlTagAttrs);
3✔
97

98
        $defaultBodyAttrs = [
3✔
99
            'class' => implode(' ', [
3✔
100
                'pid-' . $request->getAttribute('routing')->getPageId(),
3✔
101
                'layout-' . ($content['appearance']['layout'] ?? ''),
3✔
102
            ]),
3✔
103
        ];
3✔
104

105
        $rawBodyTagAttrs = GeneralUtility::get_tag_attributes(trim($typoScriptSetup['page.']['bodyTagAdd'] ?? ''));
3✔
106

107
        if ($overwriteBodyTag) {
3✔
108
            $bodyTagAttrs = array_merge($defaultBodyAttrs, $rawBodyTagAttrs);
1✔
109
        } else {
110
            $bodyTagAttrs = array_map(static function (string|array $attr) {
2✔
111
                if (is_array($attr)) {
2✔
UNCOV
112
                    return implode(' ', $attr);
×
113
                }
114

115
                return $attr;
2✔
116
            }, array_merge_recursive($defaultBodyAttrs, $rawBodyTagAttrs));
2✔
117
        }
118

119
        $content['seo']['htmlAttrs'] = array_merge([
3✔
120
            'lang' => $language->getLocale()->getLanguageCode(),
3✔
121
            'dir' => $language->getLocale()->isRightToLeftLanguageDirection() ? 'rtl' : null,
3✔
122
        ], $htmlTagAttrs);
3✔
123

124
        $content['seo']['bodyAttrs'] = $this->normalizeAttr($bodyTagAttrs);
3✔
125

126
        return $content;
3✔
127
    }
128

129
    protected function generatePageTitle(ServerRequestInterface $request, array $typoScriptConfig): string
130
    {
131
        return $this->pageTitleProviderManager->getTitle($request);
3✔
132
    }
133

134
    protected function createContentObjectRenderer(ServerRequestInterface $request, array $page): ContentObjectRenderer
135
    {
NEW
136
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
×
NEW
137
        $cObj->setRequest($request);
×
NEW
138
        $cObj->start($page, 'pages');
×
NEW
139
        return $cObj;
×
140
    }
141

142
    /**
143
     * @codeCoverageIgnore
144
     */
145
    protected function generateMetaTagsFromTyposcript(array $metaTagTypoScript, ContentObjectRenderer $cObj)
146
    {
147
        $conf = $this->typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
148
        foreach ($conf as $key => $properties) {
149
            $replace = false;
150
            if (is_array($properties)) {
151
                $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
152
                $value = trim((string)$cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.']));
153
                if ($value === '' && !empty($properties['value'])) {
154
                    $value = $properties['value'];
155
                    $replace = false;
156
                }
157
            } else {
158
                $value = $properties;
159
            }
160

161
            $attribute = 'name';
162
            if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
163
                $attribute = 'http-equiv';
164
            }
165
            if (is_array($properties) && !empty($properties['attribute'])) {
166
                $attribute = $properties['attribute'];
167
            }
168
            if (is_array($properties) && !empty($properties['replace'])) {
169
                $replace = true;
170
            }
171

172
            if (!is_array($value)) {
173
                $value = (array)$value;
174
            }
175
            foreach ($value as $subValue) {
176
                if (trim($subValue ?? '') !== '') {
177
                    $this->setMetaTag($attribute, $key, $subValue, [], $replace);
178
                }
179
            }
180
        }
181
    }
182

183
    /**
184
     * @codeCoverageIgnore
185
     */
186
    private function setMetaTag(
187
        string $type,
188
        string $name,
189
        string $content,
190
        array $subProperties = [],
191
        $replace = true
192
    ): void {
193
        $type = strtolower($type);
194
        $name = strtolower($name);
195
        if (!in_array($type, ['property', 'name', 'http-equiv'], true)) {
196
            throw new InvalidArgumentException(
197
                'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.',
198
                1496402460
199
            );
200
        }
201
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
202
        $manager->addProperty($name, $content, $subProperties, $replace, $type);
203
    }
204

205
    /**
206
     * @codeCoverageIgnore
207
     */
208
    private function normalizeAttr(array $rawHtmlAttrs): array
209
    {
210
        $htmlAttrs = [];
211

212
        foreach ($rawHtmlAttrs as $attr => $value) {
213
            $htmlAttrs[htmlspecialchars((string)$attr)] = htmlspecialchars((string)$value);
214
        }
215
        return $htmlAttrs;
216
    }
217
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc