xref: /template/strap/ComboStrap/LdJson.php (revision c30389fa7e72a1f5606ab810a79e9ef68c620e45)
1c3437056SNickeau<?php
2c3437056SNickeau
3c3437056SNickeau
4c3437056SNickeaunamespace ComboStrap;
5c3437056SNickeau
6c3437056SNickeau
7c3437056SNickeauuse action_plugin_combo_metagoogle;
804fd306cSNickeauuse ComboStrap\Meta\Api\Metadata;
904fd306cSNickeauuse ComboStrap\Meta\Api\MetadataJson;
1004fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore;
11*c30389faSgerardnicouse ComboStrap\Web\Url;
12*c30389faSgerardnicouse ComboStrap\Web\UrlEndpoint;
13c3437056SNickeau
14c3437056SNickeau/**
15c3437056SNickeau *
16c3437056SNickeau *
17c3437056SNickeau * To test locally use ngrok
18c3437056SNickeau * https://developers.google.com/search/docs/guides/debug#testing-firewalled-pages
19c3437056SNickeau *
20c3437056SNickeau * Tool:
21c3437056SNickeau * https://support.google.com/webmasters/answer/2774099# - Data Highlighter
22c3437056SNickeau * to tag page manually (you see well what kind of information they need)
23c3437056SNickeau *
24c3437056SNickeau * Ref:
25c3437056SNickeau * https://developers.google.com/search/docs/guides/intro-structured-data
26c3437056SNickeau * https://github.com/giterlizzi/dokuwiki-plugin-semantic/blob/master/helper.php
27c3437056SNickeau * https://json-ld.org/
28c3437056SNickeau * https://schema.org/docs/documents.html
29c3437056SNickeau * https://search.google.com/structured-data/testing-tool/u/0/#url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPacu_jawi
30c3437056SNickeau */
31c3437056SNickeauclass LdJson extends MetadataJson
32c3437056SNickeau{
33c3437056SNickeau
34c3437056SNickeau    public const PROPERTY_NAME = "json-ld";
35c3437056SNickeau
36c3437056SNickeau    public const SPEAKABLE = "speakable";
37c3437056SNickeau    public const NEWSARTICLE_SCHEMA_ORG_LOWERCASE = "newsarticle";
38c3437056SNickeau    public const BLOGPOSTING_SCHEMA_ORG_LOWERCASE = "blogposting";
39c3437056SNickeau    /**
40c3437056SNickeau     * @deprecated
41c3437056SNickeau     * This attribute was used to hold json-ld organization
42c3437056SNickeau     * data
43c3437056SNickeau     */
44c3437056SNickeau    public const OLD_ORGANIZATION_PROPERTY = "organization";
45c3437056SNickeau    public const DATE_PUBLISHED_KEY = "datePublished";
46c3437056SNickeau    public const DATE_MODIFIED_KEY = "dateModified";
47c3437056SNickeau
4804fd306cSNickeau    public const CANONICAL = action_plugin_combo_metagoogle::CANONICAL;
4904fd306cSNickeau
5004fd306cSNickeau    public static function createForPage(MarkupPath $page): LdJson
51c3437056SNickeau    {
52c3437056SNickeau        return (new LdJson())
53c3437056SNickeau            ->setResource($page);
54c3437056SNickeau    }
55c3437056SNickeau
56c3437056SNickeau    /**
57c3437056SNickeau     * @param array $ldJson
5804fd306cSNickeau     * @param MarkupPath $page
59c3437056SNickeau     */
6004fd306cSNickeau    public static function addImage(array &$ldJson, MarkupPath $page)
61c3437056SNickeau    {
62c3437056SNickeau        /**
63c3437056SNickeau         * Image must belong to the page
64c3437056SNickeau         * https://developers.google.com/search/docs/guides/sd-policies#images
65c3437056SNickeau         *
66c3437056SNickeau         * Image may have IPTC metadata: not yet implemented
67c3437056SNickeau         * https://developers.google.com/search/docs/advanced/appearance/image-rights-metadata
68c3437056SNickeau         *
69c3437056SNickeau         * Image must have the supported format
70c3437056SNickeau         * https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats
71c3437056SNickeau         * BMP, GIF, JPEG, PNG, WebP, and SVG
72c3437056SNickeau         */
73c3437056SNickeau        $supportedMime = [
74c3437056SNickeau            Mime::BMP,
75c3437056SNickeau            Mime::GIF,
76c3437056SNickeau            Mime::JPEG,
77c3437056SNickeau            Mime::PNG,
78c3437056SNickeau            Mime::WEBP,
79c3437056SNickeau            Mime::SVG,
80c3437056SNickeau        ];
8104fd306cSNickeau        $imagesSet = $page->getImagesForTheFollowingUsages([PageImageUsage::ALL, PageImageUsage::SOCIAL, PageImageUsage::GOOGLE]);
82c3437056SNickeau        $schemaImages = array();
8304fd306cSNickeau        foreach ($imagesSet as $pageImage) {
84c3437056SNickeau
8504fd306cSNickeau            try {
8604fd306cSNickeau                $pageImagePath = $pageImage->getSourcePath()->toWikiPath();
8704fd306cSNickeau            } catch (ExceptionCast $e) {
8804fd306cSNickeau                LogUtility::internalError("The page image should come from a wiki path", self::CANONICAL, $e);
8904fd306cSNickeau                continue;
9004fd306cSNickeau            }
9104fd306cSNickeau            try {
9204fd306cSNickeau                $mime = $pageImagePath->getMime()->toString();
9304fd306cSNickeau            } catch (ExceptionNotFound $e) {
9404fd306cSNickeau                // should not happen
9504fd306cSNickeau                LogUtility::internalError("The page image mime could not be determined. Error:" . $e->getMessage(), self::CANONICAL, $e);
9604fd306cSNickeau                $mime = "unknown";
9704fd306cSNickeau            }
98c3437056SNickeau            if (in_array($mime, $supportedMime)) {
9904fd306cSNickeau                if (FileSystems::exists($pageImagePath)) {
10004fd306cSNickeau                    try {
10104fd306cSNickeau                        $fetcherPageImage = IFetcherLocalImage::createImageFetchFromPath($pageImagePath);
10204fd306cSNickeau                    } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotExists $e) {
10304fd306cSNickeau                        LogUtility::error("The image ($pageImagePath) could not be added as page image. Error: {$e->getMessage()}");
10404fd306cSNickeau                        continue;
10504fd306cSNickeau                    }
106c3437056SNickeau                    $imageObjectSchema = array(
107c3437056SNickeau                        "@type" => "ImageObject",
10804fd306cSNickeau                        "url" => $fetcherPageImage->getFetchUrl()->toAbsoluteUrlString()
109c3437056SNickeau                    );
11004fd306cSNickeau                    if (!empty($fetcherPageImage->getIntrinsicWidth())) {
11104fd306cSNickeau                        $imageObjectSchema["width"] = $fetcherPageImage->getIntrinsicWidth();
112c3437056SNickeau                    }
11304fd306cSNickeau                    if (!empty($fetcherPageImage->getIntrinsicHeight())) {
11404fd306cSNickeau                        $imageObjectSchema["height"] = $fetcherPageImage->getIntrinsicHeight();
115c3437056SNickeau                    }
116c3437056SNickeau                    $schemaImages[] = $imageObjectSchema;
117c3437056SNickeau                } else {
11804fd306cSNickeau                    LogUtility::msg("The image ($pageImagePath) does not exist and was not added to the google ld-json", LogUtility::LVL_MSG_ERROR, action_plugin_combo_metagoogle::CANONICAL);
119c3437056SNickeau                }
120c3437056SNickeau            }
121c3437056SNickeau        }
122c3437056SNickeau
123c3437056SNickeau        if (!empty($schemaImages)) {
124c3437056SNickeau            $ldJson["image"] = $schemaImages;
125c3437056SNickeau        }
126c3437056SNickeau    }
127c3437056SNickeau
128c3437056SNickeau    public static function getName(): string
129c3437056SNickeau    {
130c3437056SNickeau        return self::PROPERTY_NAME;
131c3437056SNickeau    }
132c3437056SNickeau
13304fd306cSNickeau    static public function getPersistenceType(): string
134c3437056SNickeau    {
13504fd306cSNickeau        return MetadataDokuWikiStore::PERSISTENT_DOKUWIKI_KEY;
136c3437056SNickeau    }
137c3437056SNickeau
13804fd306cSNickeau    static public function getCanonical(): string
139c3437056SNickeau    {
140c3437056SNickeau        return action_plugin_combo_metagoogle::CANONICAL;
141c3437056SNickeau    }
142c3437056SNickeau
143c3437056SNickeau
14404fd306cSNickeau    static public function getDescription(): string
145c3437056SNickeau    {
146c3437056SNickeau        return "Advanced Page metadata definition with the json-ld format";
147c3437056SNickeau    }
148c3437056SNickeau
14904fd306cSNickeau    static public function getLabel(): string
150c3437056SNickeau    {
151c3437056SNickeau        return "Json-ld";
152c3437056SNickeau    }
153c3437056SNickeau
15404fd306cSNickeau    static public function getTab(): string
155c3437056SNickeau    {
156c3437056SNickeau        return MetaManagerForm::TAB_TYPE_VALUE;
157c3437056SNickeau    }
158c3437056SNickeau
159c3437056SNickeau
16004fd306cSNickeau    static public function isMutable(): bool
161c3437056SNickeau    {
162c3437056SNickeau        return true;
163c3437056SNickeau    }
164c3437056SNickeau
165c3437056SNickeau    public function getDefaultValue(): ?string
166c3437056SNickeau    {
167c3437056SNickeau
168c3437056SNickeau        $ldJson = $this->mergeWithDefaultValueAndGet();
169c3437056SNickeau        if ($ldJson === null) {
170c3437056SNickeau            return null;
171c3437056SNickeau        }
172c3437056SNickeau
173c3437056SNickeau        /**
174c3437056SNickeau         * Return
175c3437056SNickeau         */
176c3437056SNickeau        return Json::createFromArray($ldJson)->toPrettyJsonString();
177c3437056SNickeau
178c3437056SNickeau    }
179c3437056SNickeau
18004fd306cSNickeau    public function setFromStoreValueWithoutException($value): Metadata
181c3437056SNickeau    {
182c3437056SNickeau
183c3437056SNickeau        if ($value === null) {
184c3437056SNickeau            $resourceCombo = $this->getResource();
18504fd306cSNickeau            if (($resourceCombo instanceof MarkupPath)) {
18604fd306cSNickeau                /**
18704fd306cSNickeau                 * Deprecated, old organization syntax
18804fd306cSNickeau                 * We could add this predicate
18904fd306cSNickeau                 *
19004fd306cSNickeau                 * but we don't want to lose any data
19104fd306cSNickeau                 * (ie if the page was set to no be an organization table,
19204fd306cSNickeau                 * the frontmatter would not take it)
19304fd306cSNickeau                 */
194c3437056SNickeau                $store = $this->getReadStore();
19504fd306cSNickeau                $metadata = $store->getFromName(self::OLD_ORGANIZATION_PROPERTY);
196c3437056SNickeau                if ($metadata !== null) {
197c3437056SNickeau                    $organization = array(
198c3437056SNickeau                        "organization" => $metadata
199c3437056SNickeau                    );
200c3437056SNickeau                    $ldJsonOrganization = $this->mergeWithDefaultValueAndGet($organization);
201c3437056SNickeau                    $value = Json::createFromArray($ldJsonOrganization)->toPrettyJsonString();
202c3437056SNickeau                }
203c3437056SNickeau            }
204c3437056SNickeau        }
20504fd306cSNickeau        parent::setFromStoreValueWithoutException($value);
206c3437056SNickeau        return $this;
207c3437056SNickeau
208c3437056SNickeau
209c3437056SNickeau    }
210c3437056SNickeau
211c3437056SNickeau    /**
212c3437056SNickeau     * The ldJson value
213c3437056SNickeau     * @return false|string|null
214c3437056SNickeau     */
215c3437056SNickeau    public function getLdJsonMergedWithDefault()
216c3437056SNickeau    {
217c3437056SNickeau
21804fd306cSNickeau        try {
219c3437056SNickeau            $value = $this->getValue();
220c3437056SNickeau            try {
221c3437056SNickeau                $actualValueAsArray = Json::createFromString($value)->toArray();
22204fd306cSNickeau            } catch (ExceptionCompile $e) {
22304fd306cSNickeau                LogUtility::error("The string value is not a valid Json. Value: $value", self::CANONICAL);
224c3437056SNickeau                return $value;
225c3437056SNickeau            }
22604fd306cSNickeau        } catch (ExceptionNotFound $e) {
22704fd306cSNickeau            $actualValueAsArray = [];
228c3437056SNickeau        }
229c3437056SNickeau        $actualValueAsArray = $this->mergeWithDefaultValueAndGet($actualValueAsArray);
230c3437056SNickeau        return Json::createFromArray($actualValueAsArray)->toPrettyJsonString();
231c3437056SNickeau    }
232c3437056SNickeau
233c3437056SNickeau
234c3437056SNickeau    private function mergeWithDefaultValueAndGet($actualValue = null): ?array
235c3437056SNickeau    {
236c3437056SNickeau        $page = $this->getResource();
23704fd306cSNickeau        if (!($page instanceof MarkupPath)) {
238c3437056SNickeau            return $actualValue;
239c3437056SNickeau        }
240c3437056SNickeau
24104fd306cSNickeau        $readStore = $this->getReadStore();
24204fd306cSNickeau        $type = PageType::createForPage($page)
24304fd306cSNickeau            ->setReadStore(MetadataDokuWikiStore::class)
24404fd306cSNickeau            ->getValueOrDefault();
24504fd306cSNickeau        if (!($readStore instanceof MetadataDokuWikiStore)) {
24604fd306cSNickeau            /**
24704fd306cSNickeau             * Edge case we set the readstore because in a frontmatter,
24804fd306cSNickeau             * the type may have been set
24904fd306cSNickeau             */
25004fd306cSNickeau            try {
25104fd306cSNickeau                $type = PageType::createForPage($page)
25204fd306cSNickeau                    ->setReadStore($readStore)
25304fd306cSNickeau                    ->getValue();
25404fd306cSNickeau            } catch (ExceptionNotFound $e) {
25504fd306cSNickeau                // ok
25604fd306cSNickeau            }
25704fd306cSNickeau        }
258c3437056SNickeau        switch (strtolower($type)) {
259c3437056SNickeau            case PageType::WEBSITE_TYPE:
260c3437056SNickeau
261c3437056SNickeau                /**
262c3437056SNickeau                 * https://schema.org/WebSite
263c3437056SNickeau                 * https://developers.google.com/search/docs/data-types/sitelinks-searchbox
264c3437056SNickeau                 */
265c3437056SNickeau                $ldJson = array(
266c3437056SNickeau                    '@context' => 'https://schema.org',
267c3437056SNickeau                    '@type' => 'WebSite',
268c3437056SNickeau                    'url' => Site::getBaseUrl(),
269c3437056SNickeau                    'name' => Site::getTitle()
270c3437056SNickeau                );
271c3437056SNickeau
272c3437056SNickeau                if ($page->isRootHomePage()) {
273c3437056SNickeau
274*c30389faSgerardnico                    $target = UrlEndpoint::createDokuUrl()
275*c30389faSgerardnico                            ->addQueryParameter("do", ExecutionContext::SEARCH_ACTION)
276*c30389faSgerardnico                            ->toAbsoluteUrl()
277*c30389faSgerardnico                            ->toHtmlString()
278*c30389faSgerardnico                        . Url::AMPERSAND_URL_ENCODED_FOR_HTML . 'id={search_term_string}';
279c3437056SNickeau                    $ldJson['potentialAction'] = array(
280c3437056SNickeau                        '@type' => 'SearchAction',
281*c30389faSgerardnico                        'target' => $target,
282c3437056SNickeau                        'query-input' => 'required name=search_term_string',
283c3437056SNickeau                    );
284c3437056SNickeau                }
285c3437056SNickeau
286c3437056SNickeau                $tag = Site::getTag();
287c3437056SNickeau                if (!empty($tag)) {
288c3437056SNickeau                    $ldJson['description'] = $tag;
289c3437056SNickeau                }
290c3437056SNickeau                $siteImageUrl = Site::getLogoUrlAsPng();
291c3437056SNickeau                if (!empty($siteImageUrl)) {
292c3437056SNickeau                    $ldJson['image'] = $siteImageUrl;
293c3437056SNickeau                }
294c3437056SNickeau
295c3437056SNickeau                break;
296c3437056SNickeau
297c3437056SNickeau            case PageType::ORGANIZATION_TYPE:
298c3437056SNickeau
299c3437056SNickeau                /**
300c3437056SNickeau                 * Organization + Logo
301c3437056SNickeau                 * https://developers.google.com/search/docs/data-types/logo
302c3437056SNickeau                 */
303c3437056SNickeau                $ldJson = array(
304c3437056SNickeau                    "@context" => "https://schema.org",
305c3437056SNickeau                    "@type" => "Organization",
306c3437056SNickeau                    "url" => Site::getBaseUrl(),
307c3437056SNickeau                    "logo" => Site::getLogoUrlAsPng()
308c3437056SNickeau                );
309c3437056SNickeau
310c3437056SNickeau                break;
311c3437056SNickeau
312c3437056SNickeau            case PageType::ARTICLE_TYPE:
313c3437056SNickeau            case PageType::NEWS_TYPE:
314c3437056SNickeau            case PageType::BLOG_TYPE:
315c3437056SNickeau            case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE:
316c3437056SNickeau            case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE:
317c3437056SNickeau            case PageType::HOME_TYPE:
318c3437056SNickeau            case PageType::WEB_PAGE_TYPE:
319c3437056SNickeau
320c3437056SNickeau                switch (strtolower($type)) {
321c3437056SNickeau                    case PageType::NEWS_TYPE:
322c3437056SNickeau                    case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE:
323c3437056SNickeau                        $schemaType = "NewsArticle";
324c3437056SNickeau                        break;
325c3437056SNickeau                    case PageType::BLOG_TYPE:
326c3437056SNickeau                    case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE:
327c3437056SNickeau                        $schemaType = "BlogPosting";
328c3437056SNickeau                        break;
329c3437056SNickeau                    case PageType::HOME_TYPE:
330c3437056SNickeau                    case PageType::WEB_PAGE_TYPE:
331c3437056SNickeau                        // https://schema.org/WebPage
332c3437056SNickeau                        $schemaType = "WebPage";
333c3437056SNickeau                        break;
334c3437056SNickeau                    case PageType::ARTICLE_TYPE:
335c3437056SNickeau                    default:
336c3437056SNickeau                        $schemaType = "Article";
337c3437056SNickeau                        break;
338c3437056SNickeau
339c3437056SNickeau                }
340c3437056SNickeau                // https://developers.google.com/search/docs/data-types/article
341c3437056SNickeau                // https://schema.org/Article
342c3437056SNickeau
343c3437056SNickeau                // Image (at least 696 pixels wide)
344c3437056SNickeau                // https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats
345c3437056SNickeau                // BMP, GIF, JPEG, PNG, WebP, and SVG.
346c3437056SNickeau
347c3437056SNickeau                // Date should be https://en.wikipedia.org/wiki/ISO_8601
348c3437056SNickeau
349c3437056SNickeau
350c3437056SNickeau                $ldJson = array(
351c3437056SNickeau                    "@context" => "https://schema.org",
352c3437056SNickeau                    "@type" => $schemaType,
35304fd306cSNickeau                    'url' => $page->getAbsoluteCanonicalUrl()->toString(),
354c3437056SNickeau                    "headline" => $page->getTitleOrDefault(),
35504fd306cSNickeau
356c3437056SNickeau                );
357c3437056SNickeau
35804fd306cSNickeau                try {
35904fd306cSNickeau                    $ldJson[self::DATE_PUBLISHED_KEY] = $page
36004fd306cSNickeau                        ->getPublishedElseCreationTime()
36104fd306cSNickeau                        ->format(Iso8601Date::getFormat());
36204fd306cSNickeau                } catch (ExceptionNotFound $e) {
36304fd306cSNickeau                    // Internal error, the page should exist
36404fd306cSNickeau                    LogUtility::error("Internal Error: We were unable to define the publication date for the page ($page). Error: {$e->getMessage()}", self::CANONICAL);
36504fd306cSNickeau                }
36604fd306cSNickeau
367c3437056SNickeau                /**
368c3437056SNickeau                 * Modified Time
369c3437056SNickeau                 */
37004fd306cSNickeau                try {
371c3437056SNickeau                    $modifiedTime = $page->getModifiedTimeOrDefault();
372c3437056SNickeau                    $ldJson[self::DATE_MODIFIED_KEY] = $modifiedTime->format(Iso8601Date::getFormat());
37304fd306cSNickeau                } catch (ExceptionNotFound $e) {
37404fd306cSNickeau                    // Internal error, the page should exist
37504fd306cSNickeau                    LogUtility::error("Internal Error: We were unable to define the modification date for the page ($page)", self::CANONICAL);
37604fd306cSNickeau                }
377c3437056SNickeau
378c3437056SNickeau                /**
379c3437056SNickeau                 * Publisher info
380c3437056SNickeau                 */
381c3437056SNickeau                $publisher = array(
382c3437056SNickeau                    "@type" => "Organization",
38304fd306cSNickeau                    "name" => Site::getName()
384c3437056SNickeau                );
385c3437056SNickeau                $logoUrlAsPng = Site::getLogoUrlAsPng();
386c3437056SNickeau                if (!empty($logoUrlAsPng)) {
387c3437056SNickeau                    $publisher["logo"] = array(
388c3437056SNickeau                        "@type" => "ImageObject",
389c3437056SNickeau                        "url" => $logoUrlAsPng
390c3437056SNickeau                    );
391c3437056SNickeau                }
392c3437056SNickeau                $ldJson["publisher"] = $publisher;
393c3437056SNickeau
394c3437056SNickeau                self::addImage($ldJson, $page);
395c3437056SNickeau                break;
396c3437056SNickeau
397c3437056SNickeau            case PageType::EVENT_TYPE:
398c3437056SNickeau                // https://developers.google.com/search/docs/advanced/structured-data/event
399c3437056SNickeau                $ldJson = array(
400c3437056SNickeau                    "@context" => "https://schema.org",
401c3437056SNickeau                    "@type" => "Event");
40204fd306cSNickeau                try {
403c3437056SNickeau                    $eventName = $page->getName();
404c3437056SNickeau                    $ldJson["name"] = $eventName;
40504fd306cSNickeau                } catch (ExceptionNotFound $e) {
406c3437056SNickeau                    LogUtility::msg("The name metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
407c3437056SNickeau                    return null;
408c3437056SNickeau                }
40904fd306cSNickeau
41004fd306cSNickeau                try {
411c3437056SNickeau                    $eventDescription = $page->getDescription();
41204fd306cSNickeau                } catch (ExceptionNotFound $e) {
413c3437056SNickeau                    LogUtility::msg("The description metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
414c3437056SNickeau                    return null;
415c3437056SNickeau                }
41604fd306cSNickeau
417c3437056SNickeau                $ldJson["description"] = $eventDescription;
41804fd306cSNickeau                try {
41904fd306cSNickeau                    $startDate = $page->getStartDate();
42004fd306cSNickeau                } catch (ExceptionNotFound $e) {
421c3437056SNickeau                    LogUtility::msg("The date_start metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
422c3437056SNickeau                    return null;
423c3437056SNickeau                }
42404fd306cSNickeau                $ldJson["startDate"] = $startDate->format(Iso8601Date::getFormat());
425c3437056SNickeau
42604fd306cSNickeau                try {
42704fd306cSNickeau                    $endDate = $page->getEndDate();
42804fd306cSNickeau                } catch (ExceptionNotFound $e) {
429c3437056SNickeau                    LogUtility::msg("The date_end metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
430c3437056SNickeau                    return null;
431c3437056SNickeau                }
43204fd306cSNickeau                $ldJson["endDate"] = $endDate->format(Iso8601Date::getFormat());
433c3437056SNickeau
434c3437056SNickeau
435c3437056SNickeau                self::addImage($ldJson, $page);
436c3437056SNickeau                break;
437c3437056SNickeau
438c3437056SNickeau
439c3437056SNickeau            default:
440c3437056SNickeau
441c3437056SNickeau                // May be added manually by the user itself
442c3437056SNickeau                $ldJson = array(
443c3437056SNickeau                    '@context' => 'https://schema.org',
444c3437056SNickeau                    '@type' => $type,
44504fd306cSNickeau                    'url' => $page->getAbsoluteCanonicalUrl()->toString()
446c3437056SNickeau                );
447c3437056SNickeau                break;
448c3437056SNickeau        }
449c3437056SNickeau
450c3437056SNickeau
451c3437056SNickeau        /**
452c3437056SNickeau         * https://developers.google.com/search/docs/data-types/speakable
453c3437056SNickeau         */
454c3437056SNickeau        $speakableXpath = array();
455c3437056SNickeau        $speakableXpath[] = "/html/head/title";
45604fd306cSNickeau        try {
45704fd306cSNickeau            PageDescription::createForPage($page)
45804fd306cSNickeau                ->getValue();
459c3437056SNickeau            /**
460c3437056SNickeau             * Only the description written otherwise this is not speakable
461c3437056SNickeau             * you can have link and other strangeness
462c3437056SNickeau             */
463c3437056SNickeau            $speakableXpath[] = "/html/head/meta[@name='description']/@content";
46404fd306cSNickeau        } catch (ExceptionNotFound $e) {
46504fd306cSNickeau            // ok, no description
466c3437056SNickeau        }
467c3437056SNickeau        $ldJson[self::SPEAKABLE] = array(
468c3437056SNickeau            "@type" => "SpeakableSpecification",
469c3437056SNickeau            "xpath" => $speakableXpath
470c3437056SNickeau        );
471c3437056SNickeau
472c3437056SNickeau        /**
473c3437056SNickeau         * merge with the extra
474c3437056SNickeau         */
475c3437056SNickeau        if ($actualValue !== null) {
476c3437056SNickeau            return array_merge($ldJson, $actualValue);
477c3437056SNickeau        }
478c3437056SNickeau        return $ldJson;
479c3437056SNickeau    }
480c3437056SNickeau
481c3437056SNickeau
48204fd306cSNickeau    static public function isOnForm(): bool
48304fd306cSNickeau    {
48404fd306cSNickeau        return true;
48504fd306cSNickeau    }
48604fd306cSNickeau
487c3437056SNickeau
488c3437056SNickeau}
489