` @graph, the SEO * plugin's own graph cut down to site-level entities (Organization/Person publisher + WebSite), and the * template page's shortlink / REST / oEmbed links removed so nothing points at the hidden page. * * Rules: indexable (index / page N / category / brand / collection / budget / product / delivery) → canonical = aoo's * (self) · search / filtered listing (?price= / ?sort=) / not_found / gone / error / preview → noindex, no canonical. * * @package AooShop */ defined( 'ABSPATH' ) || exit; class AooShop_Seo { /** SEO plugins we have no adapter for — constant => name. */ const UNKNOWN_PLUGINS = array( 'AIOSEO_VERSION' => 'All in One SEO', 'SEOPRESS_VERSION' => 'SEOPress', 'THE_SEO_FRAMEWORK_VERSION' => 'The SEO Framework', 'SLIM_SEO_VER' => 'Slim SEO', 'SMARTCRAWL_VERSION' => 'SmartCrawl', 'SQ_VERSION' => 'Squirrly SEO', 'WPSSO_VERSION' => 'WPSSO', ); /** @var AooShop_Settings */ private $settings; /** @var AooShop_Renderer */ private $renderer; /** @var AooShop_Seo_Adapter|null */ private $adapter = null; /** @var array|null memoized localized graph */ private $graph = null; public function __construct( AooShop_Settings $settings, AooShop_Renderer $renderer ) { $this->settings = $settings; $this->renderer = $renderer; } public function hooks() { add_action( 'plugins_loaded', array( $this, 'boot' ), 20 ); add_action( 'wp', array( $this, 'cleanup_head' ), 20 ); add_filter( 'wp_robots', array( $this, 'wp_robots' ), 99 ); add_action( 'wp_head', array( $this, 'print_json_ld' ), 30 ); } /** 'rankmath' | 'yoast' | 'builtin' */ public static function detect() { if ( defined( 'RANK_MATH_VERSION' ) ) { return 'rankmath'; } if ( defined( 'WPSEO_VERSION' ) ) { return 'yoast'; } return 'builtin'; } /** Names of active SEO plugins without an adapter (their tags may duplicate ours). */ public static function unknown_plugins() { $out = array(); foreach ( self::UNKNOWN_PLUGINS as $const => $name ) { if ( defined( $const ) ) { $out[] = $name; } } return $out; } public function boot() { if ( $this->adapter ) { return; } $name = (string) apply_filters( 'aooshop_seo_adapter', self::detect() ); switch ( $name ) { case 'rankmath': $this->adapter = new AooShop_Seo_Rankmath( $this ); break; case 'yoast': $this->adapter = new AooShop_Seo_Yoast( $this ); break; default: $this->adapter = new AooShop_Seo_Builtin( $this ); } $this->adapter->hooks(); } public function adapter_name() { return $this->adapter ? $this->adapter->name() : self::detect(); } // ── state ────────────────────────────────────────────────────────── /** Serving an aoo route inside the template page (ok / gone / not_found / error). */ public function active() { return $this->renderer->active(); } /** Preview gate 404 for visitors. */ public function blocked() { return 'blocked' === $this->renderer->state(); } public function state() { return $this->renderer->state(); } public function route_type() { $route = $this->renderer->route(); return $route ? (string) $route['type'] : ''; } private function seo( $key ) { $payload = $this->renderer->payload(); return ( $payload && isset( $payload['seo'][ $key ] ) ) ? $payload['seo'][ $key ] : null; } /** noindex for preview / non-ok / filtered listings (`?price=` / `?sort=` / `?on_sale=1`) / aoo says noindex (search). */ public function noindex() { if ( $this->settings->is_preview() || 'ok' !== $this->state() ) { return true; } $route = $this->renderer->route(); if ( $route && ( ! empty( $route['price'] ) || ! empty( $route['sort'] ) || ! empty( $route['on_sale'] ) ) ) { return true; // Filter views are never indexed (aoo says so too — this holds even if it didn't). } return false !== strpos( (string) $this->seo( 'robots' ), 'noindex' ); } public function title() { if ( 'error' === $this->state() ) { return __( 'The shop is temporarily unavailable', 'aoo-shop' ); } return (string) $this->seo( 'title' ); } public function description() { return 'ok' === $this->state() ? trim( (string) $this->seo( 'description' ) ) : ''; } /** Absolute canonical on this site, '' when the page must not have one. */ public function canonical() { if ( $this->noindex() ) { return ''; } $url = (string) $this->seo( 'canonical' ); return '' !== $url ? $this->renderer->localize( $url ) : ''; } /** * Open Graph values (localized). url = '' when aoo has none (search / not_found / gone). * * @return array{type:string,title:string,description:string,url:string,image:string,locale:string,price_amount:string,price_currency:string,availability:string} */ public function og() { $og = $this->seo( 'og' ); $og = is_array( $og ) ? $og : array(); $ok = 'ok' === $this->state(); $url = ( $ok && ! empty( $og['url'] ) ) ? $this->renderer->localize( (string) $og['url'] ) : ''; // Indexable page: og:url must equal the canonical. $canonical = $this->canonical(); if ( '' !== $canonical ) { $url = $canonical; } return array( 'type' => ( $ok && ! empty( $og['type'] ) ) ? (string) $og['type'] : 'website', 'title' => ! empty( $og['title'] ) ? (string) $og['title'] : $this->title(), 'description' => $ok ? ( ! empty( $og['description'] ) ? (string) $og['description'] : $this->description() ) : '', 'url' => $url, 'image' => ( $ok && ! empty( $og['image'] ) ) ? (string) $og['image'] : '', 'locale' => ! empty( $og['locale'] ) ? (string) $og['locale'] : '', 'price_amount' => ( $ok && isset( $og['price_amount'] ) && '' !== (string) $og['price_amount'] ) ? (string) $og['price_amount'] : '', 'price_currency' => ( $ok && ! empty( $og['price_currency'] ) ) ? (string) $og['price_currency'] : '', 'availability' => ( $ok && ! empty( $og['availability'] ) ) ? (string) $og['availability'] : '', ); } /** * aoo breadcrumbs on this site's URLs. * * @return array */ public function breadcrumbs() { $payload = $this->renderer->payload(); $out = array(); if ( ! $payload || empty( $payload['breadcrumbs'] ) || ! is_array( $payload['breadcrumbs'] ) ) { return $out; } foreach ( $payload['breadcrumbs'] as $crumb ) { if ( ! is_array( $crumb ) || empty( $crumb['name'] ) ) { continue; } $out[] = array( 'name' => (string) $crumb['name'], 'url' => ! empty( $crumb['url'] ) ? $this->renderer->localize( (string) $crumb['url'] ) : '', ); } return $out; } /** * Entities for the single @graph (localized, @context stripped). A BreadcrumbList is built from the * payload breadcrumbs when aoo sent none (category / brand / delivery) — the SEO plugin's own one is * always removed, so this keeps exactly one. * * @return array[] */ public function graph() { if ( null !== $this->graph ) { return $this->graph; } $this->graph = array(); $payload = $this->renderer->payload(); if ( 'ok' !== $this->state() || ! $payload || empty( $payload['schema'] ) || ! is_array( $payload['schema'] ) ) { return $this->graph; } $has_breadcrumb = false; $items = array(); foreach ( $payload['schema'] as $entity ) { if ( ! is_array( $entity ) || empty( $entity['@type'] ) ) { continue; } unset( $entity['@context'] ); if ( in_array( 'BreadcrumbList', (array) $entity['@type'], true ) ) { if ( $has_breadcrumb ) { continue; } $has_breadcrumb = true; } $items[] = $entity; } $crumbs = $this->breadcrumbs(); if ( ! $has_breadcrumb && count( $crumbs ) >= 2 && 'search' !== $this->route_type() ) { $list = array(); foreach ( $crumbs as $i => $crumb ) { $el = array( '@type' => 'ListItem', 'position' => $i + 1, 'name' => $crumb['name'], ); if ( '' !== $crumb['url'] ) { $el['item'] = $crumb['url']; } $list[] = $el; } $items[] = array( '@type' => 'BreadcrumbList', 'itemListElement' => $list, ); } // Localize every aoo-built WP URL in one pass (JSON keeps slashes unescaped → plain strtr). $json = wp_json_encode( $items, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( $json ) { $decoded = json_decode( $this->renderer->localize( $json ), true ); if ( is_array( $decoded ) ) { $items = $decoded; } } $this->graph = (array) apply_filters( 'aooshop_schema_graph', $items, $payload ); return $this->graph; } /** * Keep only site-level entities of an SEO plugin's graph on aoo routes. * * @param array $entities Graph pieces (any keys). * @param string[] $keep_types @type values to keep. * @return array */ public static function site_level_only( array $entities, array $keep_types = array( 'Organization', 'WebSite' ) ) { $out = array(); foreach ( $entities as $key => $entity ) { if ( ! is_array( $entity ) || empty( $entity['@type'] ) ) { continue; } if ( array_intersect( (array) $entity['@type'], $keep_types ) ) { $out[ $key ] = $entity; } } return $out; } // ── shared output ───────────────────────────────────────────────── public function print_json_ld() { if ( ! $this->active() ) { return; } $graph = $this->graph(); if ( ! $graph ) { return; } $json = wp_json_encode( array( '@context' => 'https://schema.org', '@graph' => array_values( $graph ), ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if ( $json ) { wp_print_inline_script_tag( $json, array( 'type' => 'application/ld+json', 'class' => 'aoo-shop-schema' ) ); } } /** Nothing in the head may point at the hidden template page. */ public function cleanup_head() { if ( ! $this->active() && ! $this->blocked() ) { return; } remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 ); remove_action( 'template_redirect', 'wp_shortlink_header', 11 ); remove_action( 'wp_head', 'rest_output_link_wp_head', 10 ); remove_action( 'template_redirect', 'rest_output_link_header', 11 ); remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10 ); do_action( 'aooshop_seo_cleanup_head', $this ); } /** Core robots (built-in adapter; harmless when an SEO plugin replaces core output). */ public function wp_robots( $robots ) { if ( $this->blocked() ) { return array( 'noindex' => true, 'nofollow' => true ); } if ( ! $this->active() ) { return $robots; } unset( $robots['noindex'], $robots['nofollow'], $robots['index'], $robots['follow'] ); if ( $this->noindex() ) { $robots['noindex'] = true; unset( $robots['max-image-preview'], $robots['max-snippet'], $robots['max-video-preview'] ); } $robots['follow'] = true; return $robots; } /** og/twitter/product tags for adapters that print their own (built-in) or add the missing ones. */ public static function meta_tag( $attr, $name, $content ) { if ( '' === (string) $content ) { return; } if ( 0 === strpos( (string) $content, 'http' ) && filter_var( $content, FILTER_VALIDATE_URL ) ) { printf( '' . "\n", esc_attr( $attr ), esc_attr( $name ), esc_url( $content ) ); return; } printf( '' . "\n", esc_attr( $attr ), esc_attr( $name ), esc_attr( $content ) ); } public function print_product_og( $printer = null ) { $og = $this->og(); if ( 'product' !== $og['type'] ) { return; } $tags = array( 'product:price:amount' => $og['price_amount'], 'product:price:currency' => $og['price_currency'], 'product:availability' => $og['availability'], ); foreach ( $tags as $prop => $value ) { if ( $printer ) { call_user_func( $printer, $prop, $value ); } else { self::meta_tag( 'property', $prop, $value ); } } } } /** Base for the three head adapters. */ abstract class AooShop_Seo_Adapter { /** @var AooShop_Seo */ protected $seo; public function __construct( AooShop_Seo $seo ) { $this->seo = $seo; } abstract public function name(); abstract public function hooks(); } /** * No SEO plugin: core document title + wp_robots, our own description / canonical / og / twitter. */ class AooShop_Seo_Builtin extends AooShop_Seo_Adapter { public function name() { return 'builtin'; } public function hooks() { add_filter( 'pre_get_document_title', array( $this, 'title' ), 99 ); add_filter( 'get_canonical_url', array( $this, 'canonical' ), 99 ); add_action( 'wp', array( $this, 'prepare' ), 21 ); add_action( 'wp_head', array( $this, 'head' ), 1 ); } public function title( $title ) { if ( ! $this->seo->active() ) { return $title; } $ours = $this->seo->title(); return '' !== $ours ? $ours : $title; } public function canonical( $canonical ) { if ( ! $this->seo->active() ) { return $canonical; } $ours = $this->seo->canonical(); return '' !== $ours ? $ours : false; } public function prepare() { if ( $this->seo->active() ) { remove_action( 'wp_head', 'rel_canonical' ); // We print ours (or none) in head(). } } public function head() { if ( ! $this->seo->active() ) { return; } $desc = $this->seo->description(); if ( '' !== $desc ) { echo '' . "\n"; } $canonical = $this->seo->canonical(); if ( '' !== $canonical ) { echo '' . "\n"; } $og = $this->seo->og(); $locale = '' !== $og['locale'] ? $og['locale'] : str_replace( '-', '_', get_locale() ); AooShop_Seo::meta_tag( 'property', 'og:locale', $locale ); AooShop_Seo::meta_tag( 'property', 'og:type', $og['type'] ); AooShop_Seo::meta_tag( 'property', 'og:title', $og['title'] ); AooShop_Seo::meta_tag( 'property', 'og:description', $og['description'] ); AooShop_Seo::meta_tag( 'property', 'og:url', $og['url'] ); AooShop_Seo::meta_tag( 'property', 'og:site_name', get_bloginfo( 'name' ) ); AooShop_Seo::meta_tag( 'property', 'og:image', $og['image'] ); $this->seo->print_product_og(); AooShop_Seo::meta_tag( 'name', 'twitter:card', '' !== $og['image'] ? 'summary_large_image' : 'summary' ); AooShop_Seo::meta_tag( 'name', 'twitter:title', $og['title'] ); AooShop_Seo::meta_tag( 'name', 'twitter:description', $og['description'] ); AooShop_Seo::meta_tag( 'name', 'twitter:image', $og['image'] ); } } /** * Rank Math (filter names verified against seo-by-rank-math 1.0.276 source): * Paper: rank_math/frontend/title · /description · /canonical · /robots * OpenGraph: rank_math/opengraph/type · rank_math/opengraph/url · rank_math/opengraph/{facebook|twitter}/ * (per-tag, `:` → `_`) · rank_math/opengraph/{network}/add_images (action, Image object) · * rank_math/opengraph/{network}/image · rank_math/opengraph/slack_enhanced_data · * action rank_math/opengraph/facebook (OpenGraph object, has ->tag()) * Schema: rank_math/json_ld (keys: publisher · WebSite · place kept; WebPage, primaryImage, BreadcrumbList, * richSnippet, author… removed) * Breadcrumb: rank_math/frontend/breadcrumb/items ([text, url, 'hide_in_schema' => bool]) */ class AooShop_Seo_Rankmath extends AooShop_Seo_Adapter { const KEEP_KEYS = array( 'publisher', 'WebSite', 'place' ); /** Article / post-only Open Graph tags Rank Math adds for any singular page. */ const BLANK_FB = array( 'og_updated_time', 'article_published_time', 'article_modified_time', 'article_author', 'article_tag', 'article_section', 'article_publisher' ); public function name() { return 'rankmath'; } public function hooks() { add_filter( 'rank_math/frontend/title', array( $this, 'title' ), 99 ); add_filter( 'rank_math/frontend/description', array( $this, 'description' ), 99 ); add_filter( 'rank_math/frontend/canonical', array( $this, 'canonical' ), 99 ); add_filter( 'rank_math/frontend/robots', array( $this, 'robots' ), 99 ); add_filter( 'rank_math/opengraph/type', array( $this, 'og_type' ), 99 ); add_filter( 'rank_math/opengraph/url', array( $this, 'og_url' ), 99 ); add_filter( 'rank_math/opengraph/facebook/og_title', array( $this, 'og_title' ), 99 ); add_filter( 'rank_math/opengraph/facebook/og_description', array( $this, 'og_description' ), 99 ); add_filter( 'rank_math/opengraph/twitter/twitter_title', array( $this, 'og_title' ), 99 ); add_filter( 'rank_math/opengraph/twitter/twitter_description', array( $this, 'og_description' ), 99 ); // Image alt falls back to the template page title ("ร้านค้า (AOO Shop)") — use the page's og:title. add_filter( 'rank_math/opengraph/facebook/og_image_alt', array( $this, 'og_title' ), 99 ); foreach ( self::BLANK_FB as $tag ) { add_filter( 'rank_math/opengraph/facebook/' . $tag, array( $this, 'blank' ), 99 ); } add_filter( 'rank_math/opengraph/slack_enhanced_data', array( $this, 'slack' ), 99 ); foreach ( array( 'facebook', 'twitter' ) as $network ) { add_action( 'rank_math/opengraph/' . $network . '/add_images', array( $this, 'add_image' ), 1 ); add_filter( 'rank_math/opengraph/' . $network . '/image', array( $this, 'image' ), 99 ); } add_action( 'rank_math/opengraph/facebook', array( $this, 'product_tags' ), 31 ); add_filter( 'rank_math/json_ld', array( $this, 'json_ld' ), 9999 ); add_filter( 'rank_math/frontend/breadcrumb/items', array( $this, 'breadcrumbs' ), 99 ); } public function title( $title ) { if ( ! $this->seo->active() ) { return $title; } $ours = $this->seo->title(); return '' !== $ours ? $ours : $title; } public function description( $desc ) { return $this->seo->active() ? $this->seo->description() : $desc; } public function canonical( $canonical ) { if ( ! $this->seo->active() ) { return $canonical; } $ours = $this->seo->canonical(); return '' !== $ours ? $ours : false; } public function robots( $robots ) { if ( $this->seo->blocked() ) { return array( 'index' => 'noindex', 'follow' => 'nofollow' ); } if ( ! $this->seo->active() ) { return $robots; } $robots = is_array( $robots ) ? $robots : array(); $robots['index'] = $this->seo->noindex() ? 'noindex' : 'index'; $robots['follow'] = 'follow'; return $robots; } public function og_type( $type ) { return $this->seo->active() ? $this->seo->og()['type'] : $type; } public function og_url( $url ) { return $this->seo->active() ? $this->seo->og()['url'] : $url; } public function og_title( $title ) { return $this->seo->active() ? $this->seo->og()['title'] : $title; } public function og_description( $desc ) { return $this->seo->active() ? $this->seo->og()['description'] : $desc; } public function blank( $value ) { return $this->seo->active() ? '' : $value; } public function slack( $data ) { return $this->seo->active() ? array() : $data; } /** Make sure our image goes through Image::add_image() (the `image` filter swaps every candidate to it). */ public function add_image( $images ) { if ( $this->seo->active() && '' !== $this->seo->og()['image'] && is_object( $images ) && method_exists( $images, 'add_image' ) ) { $images->add_image(); } } public function image( $url ) { if ( ! $this->seo->active() ) { return $url; } $ours = $this->seo->og()['image']; return '' !== $ours ? $ours : $url; } public function product_tags( $og ) { if ( ! $this->seo->active() || ! is_object( $og ) || ! method_exists( $og, 'tag' ) ) { return; } $this->seo->print_product_og( function ( $prop, $value ) use ( $og ) { $og->tag( $prop, $value ); } ); } public function json_ld( $data ) { if ( ! ( $this->seo->active() || $this->seo->blocked() ) || ! is_array( $data ) ) { return $data; } return array_intersect_key( $data, array_flip( self::KEEP_KEYS ) ); } public function breadcrumbs( $crumbs ) { if ( ! $this->seo->active() ) { return $crumbs; } $ours = $this->seo->breadcrumbs(); if ( ! $ours ) { return $crumbs; } $out = array(); $home = trailingslashit( home_url() ); if ( is_array( $crumbs ) && isset( $crumbs[0][1] ) && trailingslashit( (string) $crumbs[0][1] ) === $home ) { $out[] = $crumbs[0]; // Keep the site's own "Home" crumb. } $last = count( $ours ) - 1; foreach ( $ours as $i => $crumb ) { $out[] = array( $crumb['name'], $i === $last ? '' : $crumb['url'], 'hide_in_schema' => true ); } return $out; } } /** * Yoast SEO (filter names verified against wordpress-seo 27.8 source): * wpseo_title · wpseo_metadesc · wpseo_canonical · wpseo_robots_array · wpseo_opengraph_{type,title,desc,url} · * wpseo_twitter_{title,description,image} · wpseo_frontend_presenters (drop image / article / slack / author * presenters) · action wpseo_head (our og:image + product tags) · wpseo_schema_graph (Organization/WebSite * only) · wpseo_breadcrumb_links ([url, text]). */ class AooShop_Seo_Yoast extends AooShop_Seo_Adapter { const DROP_PRESENTERS = array( 'Yoast\WP\SEO\Presenters\Open_Graph\Image_Presenter', 'Yoast\WP\SEO\Presenters\Open_Graph\Article_Published_Time_Presenter', 'Yoast\WP\SEO\Presenters\Open_Graph\Article_Modified_Time_Presenter', 'Yoast\WP\SEO\Presenters\Open_Graph\Article_Author_Presenter', 'Yoast\WP\SEO\Presenters\Open_Graph\Article_Publisher_Presenter', 'Yoast\WP\SEO\Presenters\Slack\Enhanced_Data_Presenter', 'Yoast\WP\SEO\Presenters\Meta_Author_Presenter', ); public function name() { return 'yoast'; } public function hooks() { add_filter( 'wpseo_title', array( $this, 'title' ), 99 ); add_filter( 'wpseo_metadesc', array( $this, 'description' ), 99 ); add_filter( 'wpseo_canonical', array( $this, 'canonical' ), 99 ); add_filter( 'wpseo_robots_array', array( $this, 'robots' ), 99 ); add_filter( 'wpseo_opengraph_type', array( $this, 'og_type' ), 99 ); add_filter( 'wpseo_opengraph_title', array( $this, 'og_title' ), 99 ); add_filter( 'wpseo_opengraph_desc', array( $this, 'og_description' ), 99 ); add_filter( 'wpseo_opengraph_url', array( $this, 'og_url' ), 99 ); add_filter( 'wpseo_twitter_title', array( $this, 'og_title' ), 99 ); add_filter( 'wpseo_twitter_description', array( $this, 'og_description' ), 99 ); add_filter( 'wpseo_twitter_image', array( $this, 'twitter_image' ), 99 ); add_filter( 'wpseo_frontend_presenters', array( $this, 'presenters' ), 99 ); add_action( 'wpseo_head', array( $this, 'head' ), 50 ); add_filter( 'wpseo_schema_graph', array( $this, 'schema_graph' ), 9999 ); add_filter( 'wpseo_breadcrumb_links', array( $this, 'breadcrumbs' ), 99 ); } public function title( $title ) { if ( ! $this->seo->active() ) { return $title; } $ours = $this->seo->title(); return '' !== $ours ? $ours : $title; } public function description( $desc ) { return $this->seo->active() ? $this->seo->description() : $desc; } public function canonical( $canonical ) { return $this->seo->active() ? $this->seo->canonical() : $canonical; } public function robots( $robots ) { if ( $this->seo->blocked() ) { return array( 'index' => 'noindex', 'follow' => 'nofollow' ); } if ( ! $this->seo->active() ) { return $robots; } $robots = is_array( $robots ) ? $robots : array(); if ( $this->seo->noindex() ) { unset( $robots['max-snippet'], $robots['max-image-preview'], $robots['max-video-preview'] ); $robots['index'] = 'noindex'; } else { $robots['index'] = 'index'; } $robots['follow'] = 'follow'; return $robots; } public function og_type( $type ) { return $this->seo->active() ? $this->seo->og()['type'] : $type; } public function og_title( $title ) { return $this->seo->active() ? $this->seo->og()['title'] : $title; } public function og_description( $desc ) { return $this->seo->active() ? $this->seo->og()['description'] : $desc; } public function og_url( $url ) { return $this->seo->active() ? $this->seo->og()['url'] : $url; } public function twitter_image( $url ) { if ( ! $this->seo->active() ) { return $url; } $ours = $this->seo->og()['image']; return '' !== $ours ? $ours : $url; } public function presenters( $presenters ) { if ( ! $this->seo->active() || ! is_array( $presenters ) ) { return $presenters; } $drop = self::DROP_PRESENTERS; return array_values( array_filter( $presenters, function ( $p ) use ( $drop ) { return ! in_array( get_class( $p ), $drop, true ); } ) ); } public function head() { if ( ! $this->seo->active() ) { return; } AooShop_Seo::meta_tag( 'property', 'og:image', $this->seo->og()['image'] ); $this->seo->print_product_og(); } public function schema_graph( $graph ) { if ( ! ( $this->seo->active() || $this->seo->blocked() ) || ! is_array( $graph ) ) { return $graph; } $keep = AooShop_Seo::site_level_only( $graph, array( 'Organization', 'WebSite' ) ); // Person publisher (personal sites): keep the one WebSite points to. $publisher = ''; foreach ( $keep as $piece ) { if ( in_array( 'WebSite', (array) $piece['@type'], true ) && isset( $piece['publisher']['@id'] ) ) { $publisher = (string) $piece['publisher']['@id']; } } foreach ( $graph as $key => $piece ) { if ( '' !== $publisher && is_array( $piece ) && isset( $piece['@id'] ) && $piece['@id'] === $publisher ) { $keep[ $key ] = $piece; } } ksort( $keep ); return array_values( $keep ); } public function breadcrumbs( $links ) { if ( ! $this->seo->active() ) { return $links; } $ours = $this->seo->breadcrumbs(); if ( ! $ours ) { return $links; } $out = array(); if ( is_array( $links ) && isset( $links[0]['url'] ) && trailingslashit( (string) $links[0]['url'] ) === trailingslashit( home_url() ) ) { $out[] = $links[0]; } foreach ( $ours as $crumb ) { $out[] = array( 'url' => $crumb['url'], 'text' => $crumb['name'], ); } return $out; } }