0 ) { $ids[] = $id; } } return $ids; } /** * Turns off WooCommerce markup that Orbitstem's graph already has. Call it * only while Orbitstem prints schema. * * @param bool $products Also turn off WooCommerce's product and breadcrumb * markup (Orbitstem prints fuller ones when the * WooCommerce schema setting is on). */ public static function register( $products = true ) { // A Shop page used as the front page gets a WebSite node from WooCommerce; Orbitstem's graph has one on every page. add_filter( 'woocommerce_structured_data_website', '__return_empty_array', 99 ); if ( ! $products ) { return; } add_filter( 'woocommerce_structured_data_product', '__return_empty_array', 99 ); // Orbitstem's graph already has the BreadcrumbList; a second one from WooCommerce would compete with it. add_filter( 'woocommerce_structured_data_breadcrumblist', '__return_empty_array', 99 ); } /** * Product node, or null when the post is not a WooCommerce product. * * @param WP_Post $post Post. * @param array $resolved Resolver output. * @param string $page_id WebPage @id. * @return array|null */ public static function product_node( WP_Post $post, array $resolved, $page_id ) { if ( ! self::active() || 'product' !== $post->post_type ) { return null; } $product = wc_get_product( $post->ID ); if ( ! $product ) { return null; } $url = $resolved['url']; $node = array( '@type' => 'Product', '@id' => $url . '#product', 'name' => TemplateEngine::plain( $product->get_name() ), 'url' => $url, 'mainEntityOfPage' => array( '@id' => $page_id ), ); $desc = TemplateEngine::plain( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ); if ( '' !== $desc ) { $node['description'] = mb_substr( $desc, 0, 5000 ); } $images = array(); foreach ( array_merge( array( $product->get_image_id() ), $product->get_gallery_image_ids() ) as $image_id ) { $src = $image_id ? wp_get_attachment_image_url( (int) $image_id, 'full' ) : ''; if ( $src ) { $images[] = $src; } } if ( $images ) { $node['image'] = array_slice( array_values( array_unique( $images ) ), 0, 10 ); } elseif ( '' !== $resolved['image'] ) { $node['image'] = $resolved['image']; } if ( $product->get_sku() ) { $node['sku'] = $product->get_sku(); } if ( method_exists( $product, 'get_global_unique_id' ) && $product->get_global_unique_id() ) { $node['gtin'] = $product->get_global_unique_id(); } $brand = self::brand( $product ); if ( '' !== $brand ) { $node['brand'] = array( '@type' => 'Brand', 'name' => $brand, ); } $currency = strtoupper( (string) get_woocommerce_currency() ); $availability = 'https://schema.org/' . ( $product->is_in_stock() ? ( $product->is_on_backorder() ? 'BackOrder' : 'InStock' ) : 'OutOfStock' ); // A shop currency changed by a filter to a made-up code (or a crypto symbol) is not a valid // price for search engines: the offer is left out, ratings and reviews are still shown. $valid = in_array( $currency, RichTypes::currencies(), true ); if ( $valid && $product->is_type( 'variable' ) ) { $prices = $product->get_variation_prices( true ); if ( ! empty( $prices['price'] ) ) { $node['offers'] = array( '@type' => 'AggregateOffer', 'lowPrice' => wc_format_decimal( min( $prices['price'] ), wc_get_price_decimals() ), 'highPrice' => wc_format_decimal( max( $prices['price'] ), wc_get_price_decimals() ), 'offerCount' => count( $prices['price'] ), 'priceCurrency' => $currency, 'availability' => $availability, 'url' => $url, ); } } elseif ( $valid && ! $product->is_type( 'variable' ) && '' !== $product->get_price() ) { $offer = array( '@type' => 'Offer', 'price' => wc_format_decimal( wc_get_price_to_display( $product ), wc_get_price_decimals() ), 'priceCurrency' => $currency, 'availability' => $availability, 'url' => $url, ); if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) { $offer['priceValidUntil'] = $product->get_date_on_sale_to()->date( 'Y-m-d' ); } $node['offers'] = $offer; } $count = (int) $product->get_review_count(); if ( $count > 0 && wc_review_ratings_enabled() ) { $node['aggregateRating'] = array( '@type' => 'AggregateRating', 'ratingValue' => (float) $product->get_average_rating(), 'reviewCount' => $count, ); $reviews = get_comments( array( 'post_id' => $post->ID, 'status' => 'approve', 'type' => 'review', 'number' => 5, ) ); foreach ( $reviews as $review ) { $rating = (int) get_comment_meta( $review->comment_ID, 'rating', true ); if ( $rating < 1 ) { continue; } $node['review'][] = array( '@type' => 'Review', 'reviewRating' => array( '@type' => 'Rating', 'ratingValue' => $rating, 'bestRating' => 5, ), 'author' => array( '@type' => 'Person', 'name' => TemplateEngine::plain( $review->comment_author ), ), 'datePublished' => gmdate( 'c', (int) strtotime( $review->comment_date_gmt . ' UTC' ) ), 'reviewBody' => TemplateEngine::plain( $review->comment_content ), ); } } // Google only shows product results with an offer, rating or review. return isset( $node['offers'] ) || isset( $node['aggregateRating'] ) ? $node : null; } /** * Brand from a "brand" taxonomy (WooCommerce Brands and similar) or attribute. * * @param \WC_Product $product Product. * @return string */ private static function brand( $product ) { foreach ( array( 'product_brand', 'pwb-brand', 'yith_product_brand' ) as $taxonomy ) { if ( taxonomy_exists( $taxonomy ) ) { $terms = get_the_terms( $product->get_id(), $taxonomy ); if ( is_array( $terms ) && $terms ) { return TemplateEngine::plain( $terms[0]->name ); } } } $attribute = $product->get_attribute( 'brand' ); return is_string( $attribute ) ? TemplateEngine::plain( $attribute ) : ''; } }