uct->get_id(); $global_identifier_values = get_post_meta( $product_id, 'wpseo_global_identifier_values', true ); if ( ! is_array( $global_identifier_values ) || $global_identifier_values === [] ) { return false; } foreach ( $global_identifier_values as $type => $value ) { if ( empty( $value ) ) { continue; } $this->data[ $type ] = $value; if ( $type === 'isbn' ) { if ( ! isset( $this->data['@type'] ) ) { $this->data['@type'] = 'Product'; } if ( ! is_array( $this->data['@type'] ) ) { $this->data['@type'] = [ $this->data['@type'] ]; } $this->data['@type'] = array_merge( [ 'Book' ], $this->data['@type'] ); } } return true; } /** * Update the seller attribute to reference the Organization, when it is set. * * @param array> $data Schema Product data. * * @return array> Schema Product data. */ protected function change_seller_in_offers( $data ) { $company_or_person = WPSEO_Options::get( 'company_or_person', false ); $company_name = WPSEO_Options::get( 'company_name' ); if ( $company_or_person !== 'company' || empty( $company_name ) ) { return $data; } if ( ! empty( $data['offers'] ) ) { foreach ( $data['offers'] as $key => $offer ) { $data['offers'][ $key ]['seller'] = [ '@id' => trailingslashit( YoastSEO()->meta->for_current_page()->site_url ) . Schema_IDs::ORGANIZATION_HASH, ]; } } return $data; } /** * Add brand to our output. * * @param WC_Product $product Product object. * * @return void */ private function add_brand( $product ) { $schema_brand = WPSEO_Options::get( 'woo_schema_brand' ); if ( ! empty( $schema_brand ) ) { $this->add_attribute_as( 'brand', $product, $schema_brand, 'Brand' ); } } /** * Add manufacturer to our output. * * @param WC_Product $product Product object. * * @return void */ private function add_manufacturer( $product ) { $schema_manufacturer = WPSEO_Options::get( 'woo_schema_manufacturer' ); if ( ! empty( $schema_manufacturer ) ) { $this->add_attribute_as( 'manufacturer', $product, $schema_manufacturer ); } } /** * Adds an attribute to our Product data array with the value from a taxonomy, as an Organization, * * @param string $attribute The attribute we're adding to Product. * @param WC_Product $product The WooCommerce product we're working with. * @param string $taxonomy The taxonomy to get the attribute's value from. * @param string $type The Schema type to use. * * @return void */ private function add_attribute_as( $attribute, $product, $taxonomy, $type = 'Organization' ) { $term = $this->get_primary_term_or_first_term( $taxonomy, $product->get_id() ); if ( $term !== null ) { $this->data[ $attribute ] = [ '@type' => $type, 'name' => wp_strip_all_tags( $term->name ), ]; } } /** * Adds image schema. * * @return void */ private function add_image() { /** * WooCommerce will set the image to false if none is available. This is incorrect schema and we should fix it * for our users for now. * * See https://github.com/woocommerce/woocommerce/issues/24188. */ if ( isset( $this->data['image'] ) && $this->data['image'] === false ) { unset( $this->data['image'] ); } if ( has_post_thumbnail() ) { $this->data['image'] = [ '@id' => YoastSEO()->meta->for_current_page()->canonical . Schema_IDs::PRIMARY_IMAGE_HASH, ]; return; } // Fallback to WooCommerce placeholder image. if ( function_exists( 'wc_placeholder_img_src' ) ) { $image_schema_id = YoastSEO()->meta->for_current_page()->canonical . '#woocommerceimageplaceholder'; $placeholder_img_src = wc_placeholder_img_src(); $this->data['image'] = YoastSEO()->helpers->schema->image->generate_from_url( $image_schema_id, $placeholder_img_src, '', false, false ); } } /** * Adds image schema for product variations to main node. * * @return void */ private function add_variation_images() { $image_list = []; if ( is_array( $this->variation_images ) && count( $this->variation_images ) !== 0 ) { $image_list[] = $this->data['image']; foreach ( $this->variation_images as $image ) { $image_list[] = $image; } $this->data['image'] = $image_list; } } /** * Add a custom schema property to the Schema output. * * @param WC_Product $product The product object. * @param string $option_name The option name. * @param string $schema_id The schema identifier to use. * * @return void */ private function add_custom_schema_property( $product, $option_name, $schema_id ) { $schema_data = WPSEO_Options::get( $option_name ); if ( ! empty( $schema_data ) ) { $terms = get_the_terms( $product->get_id(), $schema_data ); if ( is_array( $terms ) ) { // Variable products can have more than one color. $is_variable_product = false; if ( isset( $this->data['offers'] ) ) { foreach ( $this->data['offers'] as $offer ) { if ( $offer['@type'] === 'AggregateOffer' ) { $is_variable_product = true; } } } if ( count( $terms ) === 1 ) { $term = reset( $terms ); $this->data[ $schema_id ] = strtolower( $term->name ); } elseif ( $is_variable_product ) { $schema_data_content = []; foreach ( $terms as $term ) { $schema_data_content[] = strtolower( $term->name ); } $this->data[ $schema_id ] = $schema_data_content; } } } } /** * Adds the product color property to the Schema output. * * @param WC_Product $product The product object. * * @return void */ private function add_color( $product ) { $this->add_custom_schema_property( $product, 'woo_schema_color', 'color' ); } /** * Adds the product pattern property to the Schema output. * * @param WC_Product $product The product object. * * @return void */ private function add_pattern( $product ) { $this->add_custom_schema_property( $product, 'woo_schema_pattern', 'pattern' ); } /** * Adds the product material property to the Schema output. * * @param WC_Product $product The product object. * * @return void */ private function add_material( $product ) { $this->add_custom_schema_property( $product, 'woo_schema_material', 'material' ); } /** * Tries to get the primary term, then the first term, null if none found. * * @param string $taxonomy_name Taxonomy name for the term. * @param int $post_id Post ID for the term. * * @return WP_Term|null The primary term, the first term or null. */ protected function get_primary_term_or_first_term( $taxonomy_name, $post_id ) { $primary_term = new WPSEO_Primary_Term( $taxonomy_name, $post_id ); $primary_term_id = $primary_term->get_primary_term(); if ( $primary_term_id !== false ) { $primary_term = get_term( $primary_term_id ); if ( $primary_term instanceof WP_Term ) { return $primary_term; } } $terms = get_the_terms( $post_id, $taxonomy_name ); if ( is_array( $terms ) && count( $terms ) > 0 ) { return $terms[0]; } return null; } /** * Adds the individual product variants as variants of the offer. * * @param WC_Product $product The WooCommerce Product we're working with. * @param array> $variation The WooCommerce variation we're working with. * @param int $key The nth product variation. * * @return array> Schema Offers data. */ protected function add_individual_offer( $product, $variation, $key ) { $currency = get_woocommerce_currency(); $tax_enabled = wc_tax_enabled(); $prices_include_tax = WPSEO_WooCommerce_Utils::prices_have_tax_included(); $decimals = wc_get_price_decimals(); $product_id = $product->get_id(); $product_name = $product->get_name(); $variation_name = implode( ' / ', $variation['attributes'] ); $offer = [ '@type' => 'Offer', '@id' => YoastSEO()->meta->for_current_page()->site_url . '#/schema/offer/' . $product_id . '-' . $key, 'name' => $product_name . ' - ' . $variation_name, 'url' => get_permalink( $variation['variation_id'] ), 'priceSpecification' => [ '@type' => 'PriceSpecification', 'price' => wc_format_decimal( $variation['display_price'], $decimals ), 'priceCurrency' => $currency, ], ]; if ( $tax_enabled ) { $offer['priceSpecification']['valueAddedTaxIncluded'] = $prices_include_tax; } if ( ! $product->is_on_sale() || ! $product->get_date_on_sale_to() ) { unset( $offer['priceValidUntil'] ); } if ( $product->is_on_backorder() ) { $offer['availability'] = 'https://schema.org/PreOrder'; } /** * Filter: 'wpseo_schema_offer' - Allow changing the offer schema. * * @param array> $offer The schema offer data. * @param WC_Product_Variation $variation The WooCommerce product variation we're working with. * @param WC_Product $product The WooCommerce product we're working with. */ $data = apply_filters( 'wpseo_schema_offer', $offer, $variation, $product ); if ( is_array( $data ) ) { return $data; } return $offer; } /** * Adds the individual product variants. * * @param WC_Product $product The WooCommerce product we're working with. * @param array $variation The variation data. * @param int $key The nth product variation data. * * @return array> Schema Product data. */ protected function add_individual_product_variation( $product, $variation, $key ) { $product_id = $product->get_id(); $product_name = $product->get_name(); $product_global_ids = get_post_meta( $product_id, 'wpseo_global_identifier_values', true ); $variation_name = implode( ' / ', $variation['attributes'] ); $product_schema = [ '@type' => 'Product', '@id' => YoastSEO()->meta->for_current_page()->site_url . '#/product/' . $product_id . '-' . $key, 'name' => $product_name . ' - ' . $variation_name, 'url' => get_permalink( $variation['variation_id'] ), 'image' => $this->add_variation_image( $variation ), ]; if ( ! empty( $variation['sku'] ) ) { $product_schema['sku'] = $variation['sku']; } if ( $variation['variation_description'] !== '' ) { $product_schema['description'] = YoastSEO()->helpers->string->strip_all_tags( stripslashes( $variation['variation_description'] ) ); } // Adds variation's global identifiers to the $offer array. $variation_global_ids = get_post_meta( $variation['variation_id'], 'wpseo_variation_global_identifiers_values', true ); $global_identifier_types = [ 'gtin8', 'gtin12', 'gtin13', 'gtin14', 'mpn', ]; foreach ( $global_identifier_types as $global_identifier_type ) { if ( isset( $variation_global_ids[ $global_identifier_type ] ) && ! empty( $variation_global_ids[ $global_identifier_type ] ) ) { $product_schema[ $global_identifier_type ] = $variation_global_ids[ $global_identifier_type ]; } elseif ( isset( $product_global_ids[ $global_identifier_type ] ) && ! empty( $product_global_ids[ $global_identifier_type ] ) ) { $product_schema[ $global_identifier_type ] = $product_global_ids[ $global_identifier_type ]; } } $product_schema['offers'] = $this->add_individual_offer( $product, $variation, $key ); return $product_schema; } /** * Adds image schema for a product variation. * * @param array $variation_data The variation data. * * @return array The imageObject schema. */ private function add_variation_image( $variation_data ) { $image_schema_id = YoastSEO()->meta->for_current_page()->canonical . '#' . $variation_data['image']['title']; return YoastSEO()->helpers->schema->image->generate_from_url( $image_schema_id, $variation_data['image']['url'], $variation_data['image']['caption'] ); } /** * Enhances the review data output by WooCommerce. * * @param array> $data Review Schema data. * @param WC_Product $product The WooCommerce product we're working with. * * @return array> Review Schema data. */ protected function filter_reviews( $data, $product ) { if ( ! isset( $data['review'] ) || $data['review'] === [] ) { return $data; } $product_id = $product->get_id(); $product_name = $product->get_name(); foreach ( $data['review'] as $key => $review ) { $data['review'][ $key ]['@id'] = YoastSEO()->meta->for_current_page()->site_url . '#/schema/review/' . $product_id . '-' . $key; $data['review'][ $key ]['name'] = $product_name; } return $data; } }