setResource($page); } /** * @param array $ldJson * @param MarkupPath $page */ public static function addImage(array &$ldJson, MarkupPath $page) { /** * Image must belong to the page * https://developers.google.com/search/docs/guides/sd-policies#images * * Image may have IPTC metadata: not yet implemented * https://developers.google.com/search/docs/advanced/appearance/image-rights-metadata * * Image must have the supported format * https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats * BMP, GIF, JPEG, PNG, WebP, and SVG */ $supportedMime = [ Mime::BMP, Mime::GIF, Mime::JPEG, Mime::PNG, Mime::WEBP, Mime::SVG, ]; $imagesSet = $page->getImagesForTheFollowingUsages([PageImageUsage::ALL, PageImageUsage::SOCIAL, PageImageUsage::GOOGLE]); $schemaImages = array(); foreach ($imagesSet as $pageImage) { try { $pageImagePath = $pageImage->getSourcePath()->toWikiPath(); } catch (ExceptionCast $e) { LogUtility::internalError("The page image should come from a wiki path", self::CANONICAL, $e); continue; } try { $mime = $pageImagePath->getMime()->toString(); } catch (ExceptionNotFound $e) { // should not happen LogUtility::internalError("The page image mime could not be determined. Error:" . $e->getMessage(), self::CANONICAL, $e); $mime = "unknown"; } if (in_array($mime, $supportedMime)) { if (FileSystems::exists($pageImagePath)) { try { $fetcherPageImage = IFetcherLocalImage::createImageFetchFromPath($pageImagePath); } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotExists $e) { LogUtility::error("The image ($pageImagePath) could not be added as page image. Error: {$e->getMessage()}"); continue; } $imageObjectSchema = array( "@type" => "ImageObject", "url" => $fetcherPageImage->getFetchUrl()->toAbsoluteUrlString() ); if (!empty($fetcherPageImage->getIntrinsicWidth())) { $imageObjectSchema["width"] = $fetcherPageImage->getIntrinsicWidth(); } if (!empty($fetcherPageImage->getIntrinsicHeight())) { $imageObjectSchema["height"] = $fetcherPageImage->getIntrinsicHeight(); } $schemaImages[] = $imageObjectSchema; } else { 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); } } } if (!empty($schemaImages)) { $ldJson["image"] = $schemaImages; } } public static function getName(): string { return self::PROPERTY_NAME; } static public function getPersistenceType(): string { return MetadataDokuWikiStore::PERSISTENT_DOKUWIKI_KEY; } static public function getCanonical(): string { return action_plugin_combo_metagoogle::CANONICAL; } static public function getDescription(): string { return "Advanced Page metadata definition with the json-ld format"; } static public function getLabel(): string { return "Json-ld"; } static public function getTab(): string { return MetaManagerForm::TAB_TYPE_VALUE; } static public function isMutable(): bool { return true; } public function getDefaultValue(): ?string { $ldJson = $this->mergeWithDefaultValueAndGet(); if ($ldJson === null) { return null; } /** * Return */ return Json::createFromArray($ldJson)->toPrettyJsonString(); } public function setFromStoreValueWithoutException($value): Metadata { if ($value === null) { $resourceCombo = $this->getResource(); if (($resourceCombo instanceof MarkupPath)) { /** * Deprecated, old organization syntax * We could add this predicate * * but we don't want to lose any data * (ie if the page was set to no be an organization table, * the frontmatter would not take it) */ $store = $this->getReadStore(); $metadata = $store->getFromName(self::OLD_ORGANIZATION_PROPERTY); if ($metadata !== null) { $organization = array( "organization" => $metadata ); $ldJsonOrganization = $this->mergeWithDefaultValueAndGet($organization); $value = Json::createFromArray($ldJsonOrganization)->toPrettyJsonString(); } } } parent::setFromStoreValueWithoutException($value); return $this; } /** * The ldJson value * @return false|string|null */ public function getLdJsonMergedWithDefault() { try { $value = $this->getValue(); try { $actualValueAsArray = Json::createFromString($value)->toArray(); } catch (ExceptionCompile $e) { LogUtility::error("The string value is not a valid Json. Value: $value", self::CANONICAL); return $value; } } catch (ExceptionNotFound $e) { $actualValueAsArray = []; } $actualValueAsArray = $this->mergeWithDefaultValueAndGet($actualValueAsArray); return Json::createFromArray($actualValueAsArray)->toPrettyJsonString(); } private function mergeWithDefaultValueAndGet($actualValue = null): ?array { $page = $this->getResource(); if (!($page instanceof MarkupPath)) { return $actualValue; } $readStore = $this->getReadStore(); $type = PageType::createForPage($page) ->setReadStore(MetadataDokuWikiStore::class) ->getValueOrDefault(); if (!($readStore instanceof MetadataDokuWikiStore)) { /** * Edge case we set the readstore because in a frontmatter, * the type may have been set */ try { $type = PageType::createForPage($page) ->setReadStore($readStore) ->getValue(); } catch (ExceptionNotFound $e) { // ok } } switch (strtolower($type)) { case PageType::WEBSITE_TYPE: /** * https://schema.org/WebSite * https://developers.google.com/search/docs/data-types/sitelinks-searchbox */ $ldJson = array( '@context' => 'https://schema.org', '@type' => 'WebSite', 'url' => Site::getBaseUrl(), 'name' => Site::getTitle() ); if ($page->isRootHomePage()) { $target = UrlEndpoint::createDokuUrl() ->addQueryParameter("do", ExecutionContext::SEARCH_ACTION) ->toAbsoluteUrl() ->toHtmlString() . Url::AMPERSAND_URL_ENCODED_FOR_HTML . 'id={search_term_string}'; $ldJson['potentialAction'] = array( '@type' => 'SearchAction', 'target' => $target, 'query-input' => 'required name=search_term_string', ); } $tag = Site::getTag(); if (!empty($tag)) { $ldJson['description'] = $tag; } $siteImageUrl = Site::getLogoUrlAsPng(); if (!empty($siteImageUrl)) { $ldJson['image'] = $siteImageUrl; } break; case PageType::ORGANIZATION_TYPE: /** * Organization + Logo * https://developers.google.com/search/docs/data-types/logo */ $ldJson = array( "@context" => "https://schema.org", "@type" => "Organization", "url" => Site::getBaseUrl(), "logo" => Site::getLogoUrlAsPng() ); break; case PageType::ARTICLE_TYPE: case PageType::NEWS_TYPE: case PageType::BLOG_TYPE: case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE: case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE: case PageType::HOME_TYPE: case PageType::WEB_PAGE_TYPE: switch (strtolower($type)) { case PageType::NEWS_TYPE: case self::NEWSARTICLE_SCHEMA_ORG_LOWERCASE: $schemaType = "NewsArticle"; break; case PageType::BLOG_TYPE: case self::BLOGPOSTING_SCHEMA_ORG_LOWERCASE: $schemaType = "BlogPosting"; break; case PageType::HOME_TYPE: case PageType::WEB_PAGE_TYPE: // https://schema.org/WebPage $schemaType = "WebPage"; break; case PageType::ARTICLE_TYPE: default: $schemaType = "Article"; break; } // https://developers.google.com/search/docs/data-types/article // https://schema.org/Article // Image (at least 696 pixels wide) // https://developers.google.com/search/docs/advanced/guidelines/google-images#supported-image-formats // BMP, GIF, JPEG, PNG, WebP, and SVG. // Date should be https://en.wikipedia.org/wiki/ISO_8601 $ldJson = array( "@context" => "https://schema.org", "@type" => $schemaType, 'url' => $page->getAbsoluteCanonicalUrl()->toString(), "headline" => $page->getTitleOrDefault(), ); try { $ldJson[self::DATE_PUBLISHED_KEY] = $page ->getPublishedElseCreationTime() ->format(Iso8601Date::getFormat()); } catch (ExceptionNotFound $e) { // Internal error, the page should exist LogUtility::error("Internal Error: We were unable to define the publication date for the page ($page). Error: {$e->getMessage()}", self::CANONICAL); } /** * Modified Time */ try { $modifiedTime = $page->getModifiedTimeOrDefault(); $ldJson[self::DATE_MODIFIED_KEY] = $modifiedTime->format(Iso8601Date::getFormat()); } catch (ExceptionNotFound $e) { // Internal error, the page should exist LogUtility::error("Internal Error: We were unable to define the modification date for the page ($page)", self::CANONICAL); } /** * Publisher info */ $publisher = array( "@type" => "Organization", "name" => Site::getName() ); $logoUrlAsPng = Site::getLogoUrlAsPng(); if (!empty($logoUrlAsPng)) { $publisher["logo"] = array( "@type" => "ImageObject", "url" => $logoUrlAsPng ); } $ldJson["publisher"] = $publisher; self::addImage($ldJson, $page); break; case PageType::EVENT_TYPE: // https://developers.google.com/search/docs/advanced/structured-data/event $ldJson = array( "@context" => "https://schema.org", "@type" => "Event"); try { $eventName = $page->getName(); $ldJson["name"] = $eventName; } catch (ExceptionNotFound $e) { LogUtility::msg("The name metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); return null; } try { $eventDescription = $page->getDescription(); } catch (ExceptionNotFound $e) { LogUtility::msg("The description metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); return null; } $ldJson["description"] = $eventDescription; try { $startDate = $page->getStartDate(); } catch (ExceptionNotFound $e) { LogUtility::msg("The date_start metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); return null; } $ldJson["startDate"] = $startDate->format(Iso8601Date::getFormat()); try { $endDate = $page->getEndDate(); } catch (ExceptionNotFound $e) { LogUtility::msg("The date_end metadata is mandatory for a event page", LogUtility::LVL_MSG_ERROR, self::CANONICAL); return null; } $ldJson["endDate"] = $endDate->format(Iso8601Date::getFormat()); self::addImage($ldJson, $page); break; default: // May be added manually by the user itself $ldJson = array( '@context' => 'https://schema.org', '@type' => $type, 'url' => $page->getAbsoluteCanonicalUrl()->toString() ); break; } /** * https://developers.google.com/search/docs/data-types/speakable */ $speakableXpath = array(); $speakableXpath[] = "/html/head/title"; try { PageDescription::createForPage($page) ->getValue(); /** * Only the description written otherwise this is not speakable * you can have link and other strangeness */ $speakableXpath[] = "/html/head/meta[@name='description']/@content"; } catch (ExceptionNotFound $e) { // ok, no description } $ldJson[self::SPEAKABLE] = array( "@type" => "SpeakableSpecification", "xpath" => $speakableXpath ); /** * merge with the extra */ if ($actualValue !== null) { return array_merge($ldJson, $actualValue); } return $ldJson; } static public function isOnForm(): bool { return true; } }