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

TYPO3-Headless / headless / 25552700505

08 May 2026 11:18AM UTC coverage: 74.7% (+1.7%) from 73.04%
25552700505

Pull #881

github

web-flow
Merge cf3b72b38 into c36c6b6bf
Pull Request #881: [WIP] TYPO3 v14 support - ongoing work - not stable

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
    /**
130
     * Generate page title using PageTitleProviderManager
131
     */
132
    protected function generatePageTitle(ServerRequestInterface $request, array $typoScriptConfig): string
133
    {
134
        return $this->pageTitleProviderManager->getTitle($request);
3✔
135
    }
136

137
    /**
138
     * Create ContentObjectRenderer instance for TypoScript processing
139
     */
140
    protected function createContentObjectRenderer(ServerRequestInterface $request, array $page): ContentObjectRenderer
141
    {
NEW
142
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
×
NEW
143
        $cObj->setRequest($request);
×
NEW
144
        $cObj->start($page, 'pages');
×
NEW
145
        return $cObj;
×
146
    }
147

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

167
            $attribute = 'name';
168
            if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
169
                $attribute = 'http-equiv';
170
            }
171
            if (is_array($properties) && !empty($properties['attribute'])) {
172
                $attribute = $properties['attribute'];
173
            }
174
            if (is_array($properties) && !empty($properties['replace'])) {
175
                $replace = true;
176
            }
177

178
            if (!is_array($value)) {
179
                $value = (array)$value;
180
            }
181
            foreach ($value as $subValue) {
182
                if (trim($subValue ?? '') !== '') {
183
                    $this->setMetaTag($attribute, $key, $subValue, [], $replace);
184
                }
185
            }
186
        }
187
    }
188

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

211
    /**
212
     * @codeCoverageIgnore
213
     */
214
    private function normalizeAttr(array $rawHtmlAttrs): array
215
    {
216
        $htmlAttrs = [];
217

218
        foreach ($rawHtmlAttrs as $attr => $value) {
219
            $htmlAttrs[htmlspecialchars((string)$attr)] = htmlspecialchars((string)$value);
220
        }
221
        return $htmlAttrs;
222
    }
223
}
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