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

TYPO3-Headless / headless / 26439917307

26 May 2026 07:58AM UTC coverage: 70.455% (-5.0%) from 75.459%
26439917307

Pull #893

github

web-flow
Merge 00997f766 into 79b7c5472
Pull Request #893: [TASK] Reintroduce missing features, extension cleanup

360 of 523 new or added lines in 32 files covered. (68.83%)

167 existing lines in 7 files now uncovered.

1364 of 1936 relevant lines covered (70.45%)

7.36 hits per line

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

88.89
/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 FriendsOfTYPO3\Headless\Seo\MetaTag\AbstractMetaTagManager;
15
use InvalidArgumentException;
16
use Psr\EventDispatcher\EventDispatcherInterface;
17
use Psr\Http\Message\ServerRequestInterface;
18
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
19
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
20
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
21
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
24
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
25

26
use function array_merge;
27
use function array_merge_recursive;
28
use function htmlspecialchars;
29
use function implode;
30
use function json_decode;
31

32
use const JSON_THROW_ON_ERROR;
33

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

43
    /**
44
     * @param array<string, mixed> $content
45
     * @return array<string, mixed>
46
     */
47
    public function process(
48
        ServerRequestInterface $request,
49
        array $content
50
    ): array {
51
        $pageInformation = $request->getAttribute('frontend.page.information');
4✔
52
        if ($pageInformation === null) {
4✔
53
            return $content;
1✔
54
        }
55
        $page = $pageInformation->getPageRecord();
3✔
56

57
        $_params = ['page' => $page, 'request' => $request, '_seoLinks' => []];
3✔
58
        $_ref = null;
3✔
59
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) {
3✔
60
            GeneralUtility::callUserFunction($_funcRef, $_params, $_ref);
×
61
        }
62

63
        $typoScriptSetup = $request->getAttribute('frontend.typoscript')->getSetupArray();
3✔
64
        $typoScriptConfig = $typoScriptSetup['config.'] ?? [];
3✔
65

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

68
        $cObj = $this->createContentObjectRenderer($request, $page);
3✔
69
        $this->generateMetaTagsFromTyposcript(
3✔
70
            $typoScriptSetup['page.']['meta.'] ?? [],
3✔
71
            $cObj
3✔
72
        );
3✔
73

74
        $metaTags = [];
3✔
75
        $metaTagManagers = $this->metaTagRegistry->getAllManagers();
3✔
76

77
        foreach ($metaTagManagers as $managerObject) {
3✔
78
            if ($managerObject instanceof AbstractMetaTagManager) {
1✔
NEW
79
                $properties = $managerObject->renderAllHeadlessPropertiesAsArray();
×
80
            } else {
81
                $rendered = $managerObject->renderAllProperties();
1✔
82
                $properties = $rendered === '' ? [] : (json_decode($rendered, true, 512, JSON_THROW_ON_ERROR) ?: []);
1✔
83
            }
84

85
            if ($properties !== []) {
1✔
86
                $metaTags = array_merge($metaTags, $properties);
1✔
87
            }
88
        }
89

90
        $content['seo']['meta'] = $metaTags;
3✔
91

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

94
        $seoLinks = $_params['_seoLinks'] ?? [];
3✔
95

96
        if (count($hrefLangs) > 1) {
3✔
97
            foreach ($hrefLangs as $hrefLang => $href) {
1✔
98
                $seoLinks[] = ['rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href];
1✔
99
            }
100
        }
101

102
        if ($seoLinks !== []) {
3✔
103
            $content['seo']['link'] = $seoLinks;
1✔
104
        }
105

106
        /**
107
         * @var SiteLanguage $language
108
         */
109
        $language = $request->getAttribute('language');
3✔
110

111
        $rawHtmlTagAttrs = $typoScriptConfig['htmlTag.']['attributes.'] ?? [];
3✔
112
        $overwriteBodyTag = (int)($typoScriptConfig['headless.']['overwriteBodyTag'] ?? 0);
3✔
113
        $htmlTagAttrs = $this->normalizeAttr($rawHtmlTagAttrs);
3✔
114

115
        $defaultBodyAttrs = [
3✔
116
            'class' => implode(' ', [
3✔
117
                'pid-' . $pageInformation->getId(),
3✔
118
                'layout-' . ($content['appearance']['layout'] ?? ''),
3✔
119
            ]),
3✔
120
        ];
3✔
121

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

124
        if ($overwriteBodyTag) {
3✔
125
            $bodyTagAttrs = array_merge($defaultBodyAttrs, $rawBodyTagAttrs);
1✔
126
        } else {
127
            $bodyTagAttrs = array_map(static function (string|array $attr) {
2✔
128
                if (is_array($attr)) {
2✔
129
                    return implode(' ', $attr);
×
130
                }
131

132
                return $attr;
2✔
133
            }, array_merge_recursive($defaultBodyAttrs, $rawBodyTagAttrs));
2✔
134
        }
135

136
        $content['seo']['htmlAttrs'] = array_merge([
3✔
137
            'lang' => $language->getLocale()->getLanguageCode(),
3✔
138
            'dir' => $language->getLocale()->isRightToLeftLanguageDirection() ? 'rtl' : null,
3✔
139
        ], $htmlTagAttrs);
3✔
140

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

143
        return $content;
3✔
144
    }
145

146
    /**
147
     * @param array<string, mixed> $typoScriptConfig
148
     */
149
    protected function generatePageTitle(ServerRequestInterface $request, array $typoScriptConfig): string
150
    {
151
        return $this->pageTitleProviderManager->getTitle($request);
3✔
152
    }
153

154
    /**
155
     * @param array<string, mixed> $page
156
     */
157
    protected function createContentObjectRenderer(ServerRequestInterface $request, array $page): ContentObjectRenderer
158
    {
159
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
×
160
        $cObj->setRequest($request);
×
161
        $cObj->start($page, 'pages');
×
162
        return $cObj;
×
163
    }
164

165
    /**
166
     * @codeCoverageIgnore
167
     *
168
     * @param array<string, mixed> $metaTagTypoScript
169
     */
170
    protected function generateMetaTagsFromTyposcript(array $metaTagTypoScript, ContentObjectRenderer $cObj): void
171
    {
172
        $conf = $this->typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
173
        foreach ($conf as $key => $properties) {
174
            $replace = false;
175
            if (is_array($properties)) {
176
                $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
177
                $value = trim((string)$cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.']));
178
                if ($value === '' && !empty($properties['value'])) {
179
                    $value = $properties['value'];
180
                    $replace = false;
181
                }
182
            } else {
183
                $value = $properties;
184
            }
185

186
            $attribute = 'name';
187
            if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
188
                $attribute = 'http-equiv';
189
            }
190
            if (is_array($properties) && !empty($properties['attribute'])) {
191
                $attribute = $properties['attribute'];
192
            }
193
            if (is_array($properties) && !empty($properties['replace'])) {
194
                $replace = true;
195
            }
196

197
            if (!is_array($value)) {
198
                $value = (array)$value;
199
            }
200
            foreach ($value as $subValue) {
201
                if (trim($subValue ?? '') !== '') {
202
                    $this->setMetaTag($attribute, $key, $subValue, [], $replace);
203
                }
204
            }
205
        }
206
    }
207

208
    /**
209
     * @codeCoverageIgnore
210
     *
211
     * @param array<string, mixed> $subProperties
212
     */
213
    private function setMetaTag(
214
        string $type,
215
        string $name,
216
        string $content,
217
        array $subProperties = [],
218
        bool $replace = true
219
    ): void {
220
        $type = strtolower($type);
221
        $name = strtolower($name);
222
        if (!in_array($type, ['property', 'name', 'http-equiv'], true)) {
223
            throw new InvalidArgumentException(
224
                'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.',
225
                1496402460
226
            );
227
        }
228
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
229
        $manager->addProperty($name, $content, $subProperties, $replace, $type);
230
    }
231

232
    /**
233
     * @codeCoverageIgnore
234
     *
235
     * @param array<string, mixed> $rawHtmlAttrs
236
     * @return array<string, string>
237
     */
238
    private function normalizeAttr(array $rawHtmlAttrs): array
239
    {
240
        $htmlAttrs = [];
241

242
        foreach ($rawHtmlAttrs as $attr => $value) {
243
            $htmlAttrs[htmlspecialchars((string)$attr)] = htmlspecialchars((string)$value);
244
        }
245
        return $htmlAttrs;
246
    }
247
}
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