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