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

TYPO3-Headless / headless / 25563299292

08 May 2026 03:09PM UTC coverage: 74.7% (+1.7%) from 73.04%
25563299292

push

github

lukaszuznanski
[FEATURE] TYPO3 v14 compatibility (5.0.0-rc1)

Drops support for TYPO3 12/13. Bumps required PHP/TYPO3 versions,
removes deprecated PreviewController XClass, adds LanguageMenuProcessor
and assorted v14 adaptations across data processors, middlewares,
SEO, and tests.

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