1c3437056SNickeau<?php 2c3437056SNickeau 3c3437056SNickeau 4c3437056SNickeaunamespace ComboStrap; 5c3437056SNickeau 6c3437056SNickeau 7c3437056SNickeauuse action_plugin_combo_metagoogle; 8*04fd306cSNickeauuse ComboStrap\Meta\Api\Metadata; 9*04fd306cSNickeauuse ComboStrap\Meta\Api\MetadataJson; 10*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore; 11c3437056SNickeau 12c3437056SNickeau/** 13c3437056SNickeau * 14c3437056SNickeau * 15c3437056SNickeau * To test locally use ngrok 16c3437056SNickeau * https://developers.google.com/search/docs/guides/debug#testing-firewalled-pages 17c3437056SNickeau * 18c3437056SNickeau * Tool: 19c3437056SNickeau * https://support.google.com/webmasters/answer/2774099# - Data Highlighter 20c3437056SNickeau * to tag page manually (you see well what kind of information they need) 21c3437056SNickeau * 22c3437056SNickeau * Ref: 23c3437056SNickeau * https://developers.google.com/search/docs/guides/intro-structured-data 24c3437056SNickeau * https://github.com/giterlizzi/dokuwiki-plugin-semantic/blob/master/helper.php 25c3437056SNickeau * https://json-ld.org/ 26c3437056SNickeau * https://schema.org/docs/documents.html 27c3437056SNickeau * https://search.google.com/structured-data/testing-tool/u/0/#url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPacu_jawi 28c3437056SNickeau */ 29c3437056SNickeauclass LdJson extends MetadataJson 30c3437056SNickeau{ 31c3437056SNickeau 32c3437056SNickeau public const PROPERTY_NAME = "json-ld"; 33c3437056SNickeau 34c3437056SNickeau public const SPEAKABLE = "speakable"; 35c3437056SNickeau public const NEWSARTICLE_SCHEMA_ORG_LOWERCASE = "newsarticle"; 36c3437056SNickeau public const BLOGPOSTING_SCHEMA_ORG_LOWERCASE = "blogposting"; 37c3437056SNickeau /** 38c3437056SNickeau * @deprecated 39c3437056SNickeau * This attribute was used to hold json-ld organization 40c3437056SNickeau * data 41c3437056SNickeau */ 42c3437056SNickeau public const OLD_ORGANIZATION_PROPERTY = "organization"; 43c3437056SNickeau public const DATE_PUBLISHED_KEY = "datePublished"; 44c3437056SNickeau public const DATE_MODIFIED_KEY = "dateModified"; 45c3437056SNickeau 46*04fd306cSNickeau public const CANONICAL = action_plugin_combo_metagoogle::CANONICAL; 47*04fd306cSNickeau 48*04fd306cSNickeau public static function createForPage(MarkupPath $page): LdJson 49c3437056SNickeau { 50c3437056SNickeau return (new LdJson()) 51c3437056SNickeau ->setResource($page); 52c3437056SNickeau } 53c3437056SNickeau 54c3437056SNickeau /** 55c3437056SNickeau * @param array $ldJson 56*04fd306cSNickeau * @param MarkupPath $page 57c3437056SNickeau */ 58*04fd306cSNickeau public static function addImage(array &$ldJson, MarkupPath $page) 59c3437056SNickeau { 60c3437056SNickeau /** 61c3437056SNickeau * Image must belong to the page 62c3437056SNickeau * https://developers.google.com/search/docs/guides/sd-policies#images 63c3437056SNickeau * 64c3437056SNickeau * Image may have IPTC metadata: not yet implemented 65c3437056SNickeau * https://developers.google.com/search/docs/advanced/appearance/image-rights-metadata 66c3437056SNickeau * 67c3437056SNickeau * Image must have the supported format 68c3437056SNickeau * https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats 69c3437056SNickeau * BMP, GIF, JPEG, PNG, WebP, and SVG 70c3437056SNickeau */ 71c3437056SNickeau $supportedMime = [ 72c3437056SNickeau Mime::BMP, 73c3437056SNickeau Mime::GIF, 74c3437056SNickeau Mime::JPEG, 75c3437056SNickeau Mime::PNG, 76c3437056SNickeau Mime::WEBP, 77c3437056SNickeau Mime::SVG, 78c3437056SNickeau ]; 79*04fd306cSNickeau $imagesSet = $page->getImagesForTheFollowingUsages([PageImageUsage::ALL, PageImageUsage::SOCIAL, PageImageUsage::GOOGLE]); 80c3437056SNickeau $schemaImages = array(); 81*04fd306cSNickeau foreach ($imagesSet as $pageImage) { 82c3437056SNickeau 83*04fd306cSNickeau try { 84*04fd306cSNickeau $pageImagePath = $pageImage->getSourcePath()->toWikiPath(); 85*04fd306cSNickeau } catch (ExceptionCast $e) { 86*04fd306cSNickeau LogUtility::internalError("The page image should come from a wiki path", self::CANONICAL, $e); 87*04fd306cSNickeau continue; 88*04fd306cSNickeau } 89*04fd306cSNickeau try { 90*04fd306cSNickeau $mime = $pageImagePath->getMime()->toString(); 91*04fd306cSNickeau } catch (ExceptionNotFound $e) { 92*04fd306cSNickeau // should not happen 93*04fd306cSNickeau LogUtility::internalError("The page image mime could not be determined. Error:" . $e->getMessage(), self::CANONICAL, $e); 94*04fd306cSNickeau $mime = "unknown"; 95*04fd306cSNickeau } 96c3437056SNickeau if (in_array($mime, $supportedMime)) { 97*04fd306cSNickeau if (FileSystems::exists($pageImagePath)) { 98*04fd306cSNickeau try { 99*04fd306cSNickeau $fetcherPageImage = IFetcherLocalImage::createImageFetchFromPath($pageImagePath); 100*04fd306cSNickeau } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotExists $e) { 101*04fd306cSNickeau LogUtility::error("The image ($pageImagePath) could not be added as page image. Error: {$e->getMessage()}"); 102*04fd306cSNickeau continue; 103*04fd306cSNickeau } 104c3437056SNickeau $imageObjectSchema = array( 105c3437056SNickeau "@type" => "ImageObject", 106*04fd306cSNickeau "url" => $fetcherPageImage->getFetchUrl()->toAbsoluteUrlString() 107c3437056SNickeau ); 108*04fd306cSNickeau if (!empty($fetcherPageImage->getIntrinsicWidth())) { 109*04fd306cSNickeau $imageObjectSchema["width"] = $fetcherPageImage->getIntrinsicWidth(); 110c3437056SNickeau } 111*04fd306cSNickeau if (!empty($fetcherPageImage->getIntrinsicHeight())) { 112*04fd306cSNickeau $imageObjectSchema["height"] = $fetcherPageImage->getIntrinsicHeight(); 113c3437056SNickeau } 114c3437056SNickeau $schemaImages[] = $imageObjectSchema; 115c3437056SNickeau } else { 116*04fd306cSNickeau 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); 117c3437056SNickeau } 118c3437056SNickeau } 119c3437056SNickeau } 120c3437056SNickeau 121c3437056SNickeau if (!empty($schemaImages)) { 122c3437056SNickeau $ldJson["image"] = $schemaImages; 123c3437056SNickeau } 124c3437056SNickeau } 125c3437056SNickeau 126c3437056SNickeau public static function getName(): string 127c3437056SNickeau { 128c3437056SNickeau return self::PROPERTY_NAME; 129c3437056SNickeau } 130c3437056SNickeau 131*04fd306cSNickeau static public function getPersistenceType(): string 132c3437056SNickeau { 133*04fd306cSNickeau return MetadataDokuWikiStore::PERSISTENT_DOKUWIKI_KEY; 134c3437056SNickeau } 135c3437056SNickeau 136*04fd306cSNickeau static public function getCanonical(): string 137c3437056SNickeau { 138c3437056SNickeau return action_plugin_combo_metagoogle::CANONICAL; 139c3437056SNickeau } 140c3437056SNickeau 141c3437056SNickeau 142*04fd306cSNickeau static public function getDescription(): string 143c3437056SNickeau { 144c3437056SNickeau return "Advanced Page metadata definition with the json-ld format"; 145c3437056SNickeau } 146c3437056SNickeau 147*04fd306cSNickeau static public function getLabel(): string 148c3437056SNickeau { 149c3437056SNickeau return "Json-ld"; 150c3437056SNickeau } 151c3437056SNickeau 152*04fd306cSNickeau static public function getTab(): string 153c3437056SNickeau { 154c3437056SNickeau return MetaManagerForm::TAB_TYPE_VALUE; 155c3437056SNickeau } 156c3437056SNickeau 157c3437056SNickeau 158*04fd306cSNickeau static public function isMutable(): bool 159c3437056SNickeau { 160c3437056SNickeau return true; 161c3437056SNickeau } 162c3437056SNickeau 163c3437056SNickeau public function getDefaultValue(): ?string 164c3437056SNickeau { 165c3437056SNickeau 166c3437056SNickeau $ldJson = $this->mergeWithDefaultValueAndGet(); 167c3437056SNickeau if ($ldJson === null) { 168c3437056SNickeau return null; 169c3437056SNickeau } 170c3437056SNickeau 171c3437056SNickeau /** 172c3437056SNickeau * Return 173c3437056SNickeau */ 174c3437056SNickeau return Json::createFromArray($ldJson)->toPrettyJsonString(); 175c3437056SNickeau 176c3437056SNickeau } 177c3437056SNickeau 178*04fd306cSNickeau public function setFromStoreValueWithoutException($value): Metadata 179c3437056SNickeau { 180c3437056SNickeau 181c3437056SNickeau if ($value === null) { 182c3437056SNickeau $resourceCombo = $this->getResource(); 183*04fd306cSNickeau if (($resourceCombo instanceof MarkupPath)) { 184*04fd306cSNickeau /** 185*04fd306cSNickeau * Deprecated, old organization syntax 186*04fd306cSNickeau * We could add this predicate 187*04fd306cSNickeau * 188*04fd306cSNickeau * but we don't want to lose any data 189*04fd306cSNickeau * (ie if the page was set to no be an organization table, 190*04fd306cSNickeau * the frontmatter would not take it) 191*04fd306cSNickeau */ 192c3437056SNickeau $store = $this->getReadStore(); 193*04fd306cSNickeau $metadata = $store->getFromName(self::OLD_ORGANIZATION_PROPERTY); 194c3437056SNickeau if ($metadata !== null) { 195c3437056SNickeau $organization = array( 196c3437056SNickeau "organization" => $metadata 197c3437056SNickeau ); 198c3437056SNickeau $ldJsonOrganization = $this->mergeWithDefaultValueAndGet($organization); 199c3437056SNickeau $value = Json::createFromArray($ldJsonOrganization)->toPrettyJsonString(); 200c3437056SNickeau } 201c3437056SNickeau } 202c3437056SNickeau } 203*04fd306cSNickeau parent::setFromStoreValueWithoutException($value); 204c3437056SNickeau return $this; 205c3437056SNickeau 206c3437056SNickeau 207c3437056SNickeau } 208c3437056SNickeau 209c3437056SNickeau /** 210c3437056SNickeau * The ldJson value 211c3437056SNickeau * @return false|string|null 212c3437056SNickeau */ 213c3437056SNickeau public function getLdJsonMergedWithDefault() 214c3437056SNickeau { 215c3437056SNickeau 216*04fd306cSNickeau try { 217c3437056SNickeau $value = $this->getValue(); 218c3437056SNickeau try { 219c3437056SNickeau $actualValueAsArray = Json::createFromString($value)->toArray(); 220*04fd306cSNickeau } catch (ExceptionCompile $e) { 221*04fd306cSNickeau LogUtility::error("The string value is not a valid Json. Value: $value", self::CANONICAL); 222c3437056SNickeau return $value; 223c3437056SNickeau } 224*04fd306cSNickeau } catch (ExceptionNotFound $e) { 225*04fd306cSNickeau $actualValueAsArray = []; 226c3437056SNickeau } 227c3437056SNickeau $actualValueAsArray = $this->mergeWithDefaultValueAndGet($actualValueAsArray); 228c3437056SNickeau return Json::createFromArray($actualValueAsArray)->toPrettyJsonString(); 229c3437056SNickeau } 230c3437056SNickeau 231c3437056SNickeau 232c3437056SNickeau private function mergeWithDefaultValueAndGet($actualValue = null): ?array 233c3437056SNickeau { 234c3437056SNickeau $page = $this->getResource(); 235*04fd306cSNickeau if (!($page instanceof MarkupPath)) { 236c3437056SNickeau return $actualValue; 237c3437056SNickeau } 238c3437056SNickeau 239*04fd306cSNickeau $readStore = $this->getReadStore(); 240*04fd306cSNickeau $type = PageType::createForPage($page) 241*04fd306cSNickeau ->setReadStore(MetadataDokuWikiStore::class) 242*04fd306cSNickeau ->getValueOrDefault(); 243*04fd306cSNickeau if (!($readStore instanceof MetadataDokuWikiStore)) { 244*04fd306cSNickeau /** 245*04fd306cSNickeau * Edge case we set the readstore because in a frontmatter, 246*04fd306cSNickeau * the type may have been set 247*04fd306cSNickeau */ 248*04fd306cSNickeau try { 249*04fd306cSNickeau $type = PageType::createForPage($page) 250*04fd306cSNickeau ->setReadStore($readStore) 251*04fd306cSNickeau ->getValue(); 252*04fd306cSNickeau } catch (ExceptionNotFound $e) { 253*04fd306cSNickeau // ok 254*04fd306cSNickeau } 255*04fd306cSNickeau } 256c3437056SNickeau switch (strtolower($type)) { 257c3437056SNickeau case PageType::WEBSITE_TYPE: 258c3437056SNickeau 259c3437056SNickeau /** 260c3437056SNickeau * https://schema.org/WebSite 261c3437056SNickeau * https://developers.google.com/search/docs/data-types/sitelinks-searchbox 262c3437056SNickeau */ 263c3437056SNickeau $ldJson = array( 264c3437056SNickeau '@context' => 'https://schema.org', 265c3437056SNickeau '@type' => 'WebSite', 266c3437056SNickeau 'url' => Site::getBaseUrl(), 267c3437056SNickeau 'name' => Site::getTitle() 268c3437056SNickeau ); 269c3437056SNickeau 270c3437056SNickeau if ($page->isRootHomePage()) { 271c3437056SNickeau 272c3437056SNickeau $ldJson['potentialAction'] = array( 273c3437056SNickeau '@type' => 'SearchAction', 274c3437056SNickeau 'target' => Site::getBaseUrl() . DOKU_SCRIPT . '?do=search&id={search_term_string}', 275c3437056SNickeau 'query-input' => 'required name=search_term_string', 276c3437056SNickeau ); 277c3437056SNickeau } 278c3437056SNickeau 279c3437056SNickeau $tag = Site::getTag(); 280c3437056SNickeau if (!empty($tag)) { 281c3437056SNickeau $ldJson['description'] = $tag; 282c3437056SNickeau } 283c3437056SNickeau $siteImageUrl = Site::getLogoUrlAsPng(); 284c3437056SNickeau if (!empty($siteImageUrl)) { 285c3437056SNickeau $ldJson['image'] = $siteImageUrl; 286c3437056SNickeau } 287c3437056SNickeau 288c3437056SNickeau break; 289c3437056SNickeau 290c3437056SNickeau case PageType::ORGANIZATION_TYPE: 291c3437056SNickeau 292c3437056SNickeau /** 293c3437056SNickeau * Organization + Logo 294c3437056SNickeau * https://developers.google.com/search/docs/data-types/logo 295c3437056SNickeau */ 296c3437056SNickeau $ldJson = array( 297c3437056SNickeau "@context" => "https://schema.org", 298c3437056SNickeau "@type" => "Organization", 299c3437056SNickeau "url" => Site::getBaseUrl(), 300c3437056SNickeau "logo" => Site::getLogoUrlAsPng() 301c3437056SNickeau ); 302c3437056SNickeau 303c3437056SNickeau break; 304c3437056SNickeau 305c3437056SNickeau case PageType::ARTICLE_TYPE: 306c3437056SNickeau case PageType::NEWS_TYPE: 307c3437056SNickeau case PageType::BLOG_TYPE: 308c3437056SNickeau case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE: 309c3437056SNickeau case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE: 310c3437056SNickeau case PageType::HOME_TYPE: 311c3437056SNickeau case PageType::WEB_PAGE_TYPE: 312c3437056SNickeau 313c3437056SNickeau switch (strtolower($type)) { 314c3437056SNickeau case PageType::NEWS_TYPE: 315c3437056SNickeau case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE: 316c3437056SNickeau $schemaType = "NewsArticle"; 317c3437056SNickeau break; 318c3437056SNickeau case PageType::BLOG_TYPE: 319c3437056SNickeau case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE: 320c3437056SNickeau $schemaType = "BlogPosting"; 321c3437056SNickeau break; 322c3437056SNickeau case PageType::HOME_TYPE: 323c3437056SNickeau case PageType::WEB_PAGE_TYPE: 324c3437056SNickeau // https://schema.org/WebPage 325c3437056SNickeau $schemaType = "WebPage"; 326c3437056SNickeau break; 327c3437056SNickeau case PageType::ARTICLE_TYPE: 328c3437056SNickeau default: 329c3437056SNickeau $schemaType = "Article"; 330c3437056SNickeau break; 331c3437056SNickeau 332c3437056SNickeau } 333c3437056SNickeau // https://developers.google.com/search/docs/data-types/article 334c3437056SNickeau // https://schema.org/Article 335c3437056SNickeau 336c3437056SNickeau // Image (at least 696 pixels wide) 337c3437056SNickeau // https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats 338c3437056SNickeau // BMP, GIF, JPEG, PNG, WebP, and SVG. 339c3437056SNickeau 340c3437056SNickeau // Date should be https://en.wikipedia.org/wiki/ISO_8601 341c3437056SNickeau 342c3437056SNickeau 343c3437056SNickeau $ldJson = array( 344c3437056SNickeau "@context" => "https://schema.org", 345c3437056SNickeau "@type" => $schemaType, 346*04fd306cSNickeau 'url' => $page->getAbsoluteCanonicalUrl()->toString(), 347c3437056SNickeau "headline" => $page->getTitleOrDefault(), 348*04fd306cSNickeau 349c3437056SNickeau ); 350c3437056SNickeau 351*04fd306cSNickeau try { 352*04fd306cSNickeau $ldJson[self::DATE_PUBLISHED_KEY] = $page 353*04fd306cSNickeau ->getPublishedElseCreationTime() 354*04fd306cSNickeau ->format(Iso8601Date::getFormat()); 355*04fd306cSNickeau } catch (ExceptionNotFound $e) { 356*04fd306cSNickeau // Internal error, the page should exist 357*04fd306cSNickeau LogUtility::error("Internal Error: We were unable to define the publication date for the page ($page). Error: {$e->getMessage()}", self::CANONICAL); 358*04fd306cSNickeau } 359*04fd306cSNickeau 360c3437056SNickeau /** 361c3437056SNickeau * Modified Time 362c3437056SNickeau */ 363*04fd306cSNickeau try { 364c3437056SNickeau $modifiedTime = $page->getModifiedTimeOrDefault(); 365c3437056SNickeau $ldJson[self::DATE_MODIFIED_KEY] = $modifiedTime->format(Iso8601Date::getFormat()); 366*04fd306cSNickeau } catch (ExceptionNotFound $e) { 367*04fd306cSNickeau // Internal error, the page should exist 368*04fd306cSNickeau LogUtility::error("Internal Error: We were unable to define the modification date for the page ($page)", self::CANONICAL); 369*04fd306cSNickeau } 370c3437056SNickeau 371c3437056SNickeau /** 372c3437056SNickeau * Publisher info 373c3437056SNickeau */ 374c3437056SNickeau $publisher = array( 375c3437056SNickeau "@type" => "Organization", 376*04fd306cSNickeau "name" => Site::getName() 377c3437056SNickeau ); 378c3437056SNickeau $logoUrlAsPng = Site::getLogoUrlAsPng(); 379c3437056SNickeau if (!empty($logoUrlAsPng)) { 380c3437056SNickeau $publisher["logo"] = array( 381c3437056SNickeau "@type" => "ImageObject", 382c3437056SNickeau "url" => $logoUrlAsPng 383c3437056SNickeau ); 384c3437056SNickeau } 385c3437056SNickeau $ldJson["publisher"] = $publisher; 386c3437056SNickeau 387c3437056SNickeau self::addImage($ldJson, $page); 388c3437056SNickeau break; 389c3437056SNickeau 390c3437056SNickeau case PageType::EVENT_TYPE: 391c3437056SNickeau // https://developers.google.com/search/docs/advanced/structured-data/event 392c3437056SNickeau $ldJson = array( 393c3437056SNickeau "@context" => "https://schema.org", 394c3437056SNickeau "@type" => "Event"); 395*04fd306cSNickeau try { 396c3437056SNickeau $eventName = $page->getName(); 397c3437056SNickeau $ldJson["name"] = $eventName; 398*04fd306cSNickeau } catch (ExceptionNotFound $e) { 399c3437056SNickeau LogUtility::msg("The name metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 400c3437056SNickeau return null; 401c3437056SNickeau } 402*04fd306cSNickeau 403*04fd306cSNickeau try { 404c3437056SNickeau $eventDescription = $page->getDescription(); 405*04fd306cSNickeau } catch (ExceptionNotFound $e) { 406c3437056SNickeau LogUtility::msg("The description metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 407c3437056SNickeau return null; 408c3437056SNickeau } 409*04fd306cSNickeau 410c3437056SNickeau $ldJson["description"] = $eventDescription; 411*04fd306cSNickeau try { 412*04fd306cSNickeau $startDate = $page->getStartDate(); 413*04fd306cSNickeau } catch (ExceptionNotFound $e) { 414c3437056SNickeau LogUtility::msg("The date_start metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 415c3437056SNickeau return null; 416c3437056SNickeau } 417*04fd306cSNickeau $ldJson["startDate"] = $startDate->format(Iso8601Date::getFormat()); 418c3437056SNickeau 419*04fd306cSNickeau try { 420*04fd306cSNickeau $endDate = $page->getEndDate(); 421*04fd306cSNickeau } catch (ExceptionNotFound $e) { 422c3437056SNickeau LogUtility::msg("The date_end metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 423c3437056SNickeau return null; 424c3437056SNickeau } 425*04fd306cSNickeau $ldJson["endDate"] = $endDate->format(Iso8601Date::getFormat()); 426c3437056SNickeau 427c3437056SNickeau 428c3437056SNickeau self::addImage($ldJson, $page); 429c3437056SNickeau break; 430c3437056SNickeau 431c3437056SNickeau 432c3437056SNickeau default: 433c3437056SNickeau 434c3437056SNickeau // May be added manually by the user itself 435c3437056SNickeau $ldJson = array( 436c3437056SNickeau '@context' => 'https://schema.org', 437c3437056SNickeau '@type' => $type, 438*04fd306cSNickeau 'url' => $page->getAbsoluteCanonicalUrl()->toString() 439c3437056SNickeau ); 440c3437056SNickeau break; 441c3437056SNickeau } 442c3437056SNickeau 443c3437056SNickeau 444c3437056SNickeau /** 445c3437056SNickeau * https://developers.google.com/search/docs/data-types/speakable 446c3437056SNickeau */ 447c3437056SNickeau $speakableXpath = array(); 448c3437056SNickeau $speakableXpath[] = "/html/head/title"; 449*04fd306cSNickeau try { 450*04fd306cSNickeau PageDescription::createForPage($page) 451*04fd306cSNickeau ->getValue(); 452c3437056SNickeau /** 453c3437056SNickeau * Only the description written otherwise this is not speakable 454c3437056SNickeau * you can have link and other strangeness 455c3437056SNickeau */ 456c3437056SNickeau $speakableXpath[] = "/html/head/meta[@name='description']/@content"; 457*04fd306cSNickeau } catch (ExceptionNotFound $e) { 458*04fd306cSNickeau // ok, no description 459c3437056SNickeau } 460c3437056SNickeau $ldJson[self::SPEAKABLE] = array( 461c3437056SNickeau "@type" => "SpeakableSpecification", 462c3437056SNickeau "xpath" => $speakableXpath 463c3437056SNickeau ); 464c3437056SNickeau 465c3437056SNickeau /** 466c3437056SNickeau * merge with the extra 467c3437056SNickeau */ 468c3437056SNickeau if ($actualValue !== null) { 469c3437056SNickeau return array_merge($ldJson, $actualValue); 470c3437056SNickeau } 471c3437056SNickeau return $ldJson; 472c3437056SNickeau } 473c3437056SNickeau 474c3437056SNickeau 475*04fd306cSNickeau static public function isOnForm(): bool 476*04fd306cSNickeau { 477*04fd306cSNickeau return true; 478*04fd306cSNickeau } 479*04fd306cSNickeau 480c3437056SNickeau 481c3437056SNickeau} 482