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

TYPO3-Headless / headless / 12931869720

23 Jan 2025 02:57PM UTC coverage: 72.915% (+0.1%) from 72.782%
12931869720

push

github

web-flow
[FEATURE] Add support for `config.htmlTag` & `page.bodyTagAdd` (#802)

- By default generate language attributes for html tag based on current language page, so nuxt does not have to do it manually
- support custom attributes like `config.htmlTag.attributes.class = no-js` with option to override default attributes

- Add support for setting body tag attributes via `page.bodyTagAdd` Typoscript directive, so frontend app can automatically apply it via nuxt feature

10 of 11 new or added lines in 1 file covered. (90.91%)

1093 of 1499 relevant lines covered (72.92%)

8.35 hits per line

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

97.22
/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\Site\Entity\SiteLanguage;
19
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
22
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
23
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
24

25
use function htmlspecialchars;
26

27
class MetaHandler
28
{
29
    public function __construct(
30
        private readonly MetaTagManagerRegistry $metaTagRegistry,
31
        private readonly EventDispatcherInterface $eventDispatcher,
32
    ) {}
30✔
33

34
    public function process(ServerRequestInterface $request, TypoScriptFrontendController $controller, array $content): array
35
    {
36
        $_params = ['page' => $controller->page, 'request' => $request, '_seoLinks' => []];
2✔
37
        $_ref = null;
2✔
38
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) {
2✔
39
            GeneralUtility::callUserFunction($_funcRef, $_params, $_ref);
1✔
40
        }
41

42
        $content['seo']['title'] = $controller->generatePageTitle($request);
2✔
43

44
        $this->generateMetaTagsFromTyposcript(
2✔
45
            $controller->pSetup['meta.'] ?? [],
2✔
46
            $controller->cObj
2✔
47
        );
2✔
48

49
        $metaTags = [];
2✔
50
        $metaTagManagers = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getAllManagers();
2✔
51

52
        foreach ($metaTagManagers as $manager => $managerObject) {
2✔
53
            $properties = json_decode($managerObject->renderAllProperties(), true);
2✔
54
            if (!empty($properties)) {
2✔
55
                $metaTags = array_merge($metaTags, $properties);
1✔
56
            }
57
        }
58

59
        $content['seo']['meta'] = $metaTags;
2✔
60

61
        $hrefLangs = $this->eventDispatcher->dispatch(new ModifyHrefLangTagsEvent($request))->getHrefLangs();
2✔
62

63
        $seoLinks = $_params['_seoLinks'] ?? [];
2✔
64

65
        if (count($hrefLangs) > 1) {
2✔
66
            foreach ($hrefLangs as $hrefLang => $href) {
1✔
67
                $seoLinks[] = ['rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href];
1✔
68
            }
69
        }
70

71
        if ($seoLinks !== []) {
2✔
72
            $content['seo']['link'] = $seoLinks;
1✔
73
        }
74

75
        /**
76
         * @var SiteLanguage $language
77
         */
78
        $language = $request->getAttribute('language');
2✔
79

80
        $rawHtmlTagAttrs = $controller->config['config']['htmlTag.']['attributes.'] ?? [];
2✔
81
        $htmlTagAttrs = $this->normalizeAttr($rawHtmlTagAttrs);
2✔
82

83
        $rawBodyTagAttrs = GeneralUtility::get_tag_attributes(trim($request->getAttribute('frontend.typoscript')->getSetupArray()['page.']['bodyTagAdd'] ?? ''));
2✔
84
        $bodyTagAttrs = $this->normalizeAttr($rawBodyTagAttrs);
2✔
85

86
        $content['seo']['htmlAttrs'] = array_merge([
2✔
87
            'lang' => $language->getLocale()->getLanguageCode(),
2✔
88
            'dir' => $language->getLocale()->isRightToLeftLanguageDirection() ? 'rtl' : null,
2✔
89
        ], $htmlTagAttrs);
2✔
90

91
        if ($bodyTagAttrs !== []) {
2✔
NEW
92
            $content['seo']['bodyAttrs'] = $bodyTagAttrs;
×
93
        }
94

95
        return $content;
2✔
96
    }
97

98
    /**
99
     * @codeCoverageIgnore
100
     */
101
    protected function generateMetaTagsFromTyposcript(array $metaTagTypoScript, ContentObjectRenderer $cObj)
102
    {
103
        $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
104
        $conf = $typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
105
        foreach ($conf as $key => $properties) {
106
            $replace = false;
107
            if (is_array($properties)) {
108
                $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
109
                $value = trim((string)$cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.']));
110
                if ($value === '' && !empty($properties['value'])) {
111
                    $value = $properties['value'];
112
                    $replace = false;
113
                }
114
            } else {
115
                $value = $properties;
116
            }
117

118
            $attribute = 'name';
119
            if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
120
                $attribute = 'http-equiv';
121
            }
122
            if (is_array($properties) && !empty($properties['attribute'])) {
123
                $attribute = $properties['attribute'];
124
            }
125
            if (is_array($properties) && !empty($properties['replace'])) {
126
                $replace = true;
127
            }
128

129
            if (!is_array($value)) {
130
                $value = (array)$value;
131
            }
132
            foreach ($value as $subValue) {
133
                if (trim($subValue ?? '') !== '') {
134
                    $this->setMetaTag($attribute, $key, $subValue, [], $replace);
135
                }
136
            }
137
        }
138
    }
139

140
    /**
141
     * @codeCoverageIgnore
142
     */
143
    private function setMetaTag(string $type, string $name, string $content, array $subProperties = [], $replace = true): void
144
    {
145
        $type = strtolower($type);
146
        $name = strtolower($name);
147
        if (!in_array($type, ['property', 'name', 'http-equiv'], true)) {
148
            throw new InvalidArgumentException(
149
                'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.',
150
                1496402460
151
            );
152
        }
153
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
154
        $manager->addProperty($name, $content, $subProperties, $replace, $type);
155
    }
156

157
    /**
158
     * @codeCoverageIgnore
159
     */
160
    private function normalizeAttr(array $rawHtmlAttrs): array
161
    {
162
        $htmlAttrs = [];
163

164
        foreach ($rawHtmlAttrs as $attr => $value) {
165
            $htmlAttrs[htmlspecialchars((string)$attr)] = htmlspecialchars((string)$value);
166
        }
167
        return $htmlAttrs;
168
    }
169
}
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

© 2025 Coveralls, Inc