| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- <?php
- /**
- * Structured data's handler and generator using JSON-LD format.
- *
- * @package WooCommerce/Classes
- * @since 3.0.0
- * @version 3.0.0
- */
- defined( 'ABSPATH' ) || exit;
- /**
- * Structured data class.
- */
- class WC_Structured_Data {
- /**
- * Stores the structured data.
- *
- * @var array $_data Array of structured data.
- */
- private $_data = array();
- /**
- * Constructor.
- */
- public function __construct() {
- // Generate structured data.
- add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 );
- add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 );
- add_action( 'woocommerce_shop_loop', array( $this, 'generate_product_data' ), 10 );
- add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 );
- add_action( 'woocommerce_review_meta', array( $this, 'generate_review_data' ), 20 );
- add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 );
- // Output structured data.
- add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 );
- add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 );
- }
- /**
- * Sets data.
- *
- * @param array $data Structured data.
- * @param bool $reset Unset data (default: false).
- * @return bool
- */
- public function set_data( $data, $reset = false ) {
- if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) {
- return false;
- }
- if ( $reset && isset( $this->_data ) ) {
- unset( $this->_data );
- }
- $this->_data[] = $data;
- return true;
- }
- /**
- * Gets data.
- *
- * @return array
- */
- public function get_data() {
- return $this->_data;
- }
- /**
- * Structures and returns data.
- *
- * List of types available by default for specific request:
- *
- * 'product',
- * 'review',
- * 'breadcrumblist',
- * 'website',
- * 'order',
- *
- * @param array $types Structured data types.
- * @return array
- */
- public function get_structured_data( $types ) {
- $data = array();
- // Put together the values of same type of structured data.
- foreach ( $this->get_data() as $value ) {
- $data[ strtolower( $value['@type'] ) ][] = $value;
- }
- // Wrap the multiple values of each type inside a graph... Then add context to each type.
- foreach ( $data as $type => $value ) {
- $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0];
- $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ];
- }
- // If requested types, pick them up... Finally change the associative array to an indexed one.
- $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data );
- if ( ! empty( $data ) ) {
- if ( 1 < count( $data ) ) {
- $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data );
- } else {
- $data = $data[0];
- }
- }
- return $data;
- }
- /**
- * Get data types for pages.
- *
- * @return array
- */
- protected function get_data_type_for_page() {
- $types = array();
- $types[] = is_shop() || is_product_category() || is_product() ? 'product' : '';
- $types[] = is_shop() && is_front_page() ? 'website' : '';
- $types[] = is_product() ? 'review' : '';
- $types[] = ! is_shop() ? 'breadcrumblist' : '';
- $types[] = 'order';
- return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) );
- }
- /**
- * Makes sure email structured data only outputs on non-plain text versions.
- *
- * @param WP_Order $order Order data.
- * @param bool $sent_to_admin Send to admin (default: false).
- * @param bool $plain_text Plain text email (default: false).
- */
- public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) {
- if ( $plain_text ) {
- return;
- }
- echo '<div style="display: none; font-size: 0; max-height: 0; line-height: 0; padding: 0; mso-hide: all;">';
- $this->output_structured_data();
- echo '</div>';
- }
- /**
- * Sanitizes, encodes and outputs structured data.
- *
- * Hooked into `wp_footer` action hook.
- * Hooked into `woocommerce_email_order_details` action hook.
- */
- public function output_structured_data() {
- $types = $this->get_data_type_for_page();
- $data = wc_clean( $this->get_structured_data( $types ) );
- if ( $data ) {
- echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>';
- }
- }
- /*
- |--------------------------------------------------------------------------
- | Generators
- |--------------------------------------------------------------------------
- |
- | Methods for generating specific structured data types:
- |
- | - Product
- | - Review
- | - BreadcrumbList
- | - WebSite
- | - Order
- |
- | The generated data is stored into `$this->_data`.
- | See the methods above for handling `$this->_data`.
- |
- */
- /**
- * Generates Product structured data.
- *
- * Hooked into `woocommerce_single_product_summary` action hook.
- * Hooked into `woocommerce_shop_loop` action hook.
- *
- * @param WC_Product $product Product data (default: null).
- */
- public function generate_product_data( $product = null ) {
- if ( ! is_object( $product ) ) {
- global $product;
- }
- if ( ! is_a( $product, 'WC_Product' ) ) {
- return;
- }
- $shop_name = get_bloginfo( 'name' );
- $shop_url = home_url();
- $currency = get_woocommerce_currency();
- $markup = array(
- '@type' => 'Product',
- '@id' => get_permalink( $product->get_id() ),
- 'name' => $product->get_name(),
- );
- if ( apply_filters( 'woocommerce_structured_data_product_limit', is_product_taxonomy() || is_shop() ) ) {
- $markup['url'] = $markup['@id'];
- $this->set_data( apply_filters( 'woocommerce_structured_data_product_limited', $markup, $product ) );
- return;
- }
- $markup['image'] = wp_get_attachment_url( $product->get_image_id() );
- $markup['description'] = wpautop( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) );
- $markup['sku'] = $product->get_sku();
- if ( '' !== $product->get_price() ) {
- if ( $product->is_type( 'variable' ) ) {
- $lowest = $product->get_variation_price( 'min', false );
- $highest = $product->get_variation_price( 'max', false );
- if ( $lowest === $highest ) {
- $markup_offer = array(
- '@type' => 'Offer',
- 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
- 'priceSpecification' => array(
- 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
- 'priceCurrency' => $currency,
- 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false',
- ),
- );
- } else {
- $markup_offer = array(
- '@type' => 'AggregateOffer',
- 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
- 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ),
- );
- }
- } else {
- $markup_offer = array(
- '@type' => 'Offer',
- 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),
- 'priceSpecification' => array(
- 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),
- 'priceCurrency' => $currency,
- 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false',
- ),
- );
- }
- $markup_offer += array(
- 'priceCurrency' => $currency,
- 'availability' => 'https://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ),
- 'url' => $markup['@id'],
- 'seller' => array(
- '@type' => 'Organization',
- 'name' => $shop_name,
- 'url' => $shop_url,
- ),
- );
- $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) );
- }
- if ( $product->get_review_count() && 'yes' === get_option( 'woocommerce_enable_review_rating' ) ) {
- $markup['aggregateRating'] = array(
- '@type' => 'AggregateRating',
- 'ratingValue' => $product->get_average_rating(),
- 'reviewCount' => $product->get_review_count(),
- );
- }
- $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
- }
- /**
- * Generates Review structured data.
- *
- * Hooked into `woocommerce_review_meta` action hook.
- *
- * @param WP_Comment $comment Comment data.
- */
- public function generate_review_data( $comment ) {
- $markup = array();
- $markup['@type'] = 'Review';
- $markup['@id'] = get_comment_link( $comment->comment_ID );
- $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID );
- $markup['description'] = get_comment_text( $comment->comment_ID );
- $markup['itemReviewed'] = array(
- '@type' => 'Product',
- 'name' => get_the_title( $comment->comment_post_ID ),
- );
- // Skip replies unless they have a rating.
- $rating = get_comment_meta( $comment->comment_ID, 'rating', true );
- if ( $rating ) {
- $markup['reviewRating'] = array(
- '@type' => 'rating',
- 'ratingValue' => $rating,
- );
- } elseif ( $comment->comment_parent ) {
- return;
- }
- $markup['author'] = array(
- '@type' => 'Person',
- 'name' => get_comment_author( $comment->comment_ID ),
- );
- $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) );
- }
- /**
- * Generates BreadcrumbList structured data.
- *
- * Hooked into `woocommerce_breadcrumb` action hook.
- *
- * @param WC_Breadcrumb $breadcrumbs Breadcrumb data.
- */
- public function generate_breadcrumblist_data( $breadcrumbs ) {
- $crumbs = $breadcrumbs->get_breadcrumb();
- if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
- return;
- }
- $markup = array();
- $markup['@type'] = 'BreadcrumbList';
- $markup['itemListElement'] = array();
- foreach ( $crumbs as $key => $crumb ) {
- $markup['itemListElement'][ $key ] = array(
- '@type' => 'ListItem',
- 'position' => $key + 1,
- 'item' => array(
- 'name' => $crumb[0],
- ),
- );
- if ( ! empty( $crumb[1] ) && count( $crumbs ) !== $key + 1 ) {
- $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] );
- }
- }
- $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) );
- }
- /**
- * Generates WebSite structured data.
- *
- * Hooked into `woocommerce_before_main_content` action hook.
- */
- public function generate_website_data() {
- $markup = array();
- $markup['@type'] = 'WebSite';
- $markup['name'] = get_bloginfo( 'name' );
- $markup['url'] = home_url();
- $markup['potentialAction'] = array(
- '@type' => 'SearchAction',
- 'target' => home_url( '?s={search_term_string}&post_type=product' ),
- 'query-input' => 'required name=search_term_string',
- );
- $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
- }
- /**
- * Generates Order structured data.
- *
- * Hooked into `woocommerce_email_order_details` action hook.
- *
- * @param WP_Order $order Order data.
- * @param bool $sent_to_admin Send to admin (default: false).
- * @param bool $plain_text Plain text email (default: false).
- */
- public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
- if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) {
- return;
- }
- $shop_name = get_bloginfo( 'name' );
- $shop_url = home_url();
- $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url();
- $order_statuses = array(
- 'pending' => 'https://schema.org/OrderPaymentDue',
- 'processing' => 'https://schema.org/OrderProcessing',
- 'on-hold' => 'https://schema.org/OrderProblem',
- 'completed' => 'https://schema.org/OrderDelivered',
- 'cancelled' => 'https://schema.org/OrderCancelled',
- 'refunded' => 'https://schema.org/OrderReturned',
- 'failed' => 'https://schema.org/OrderProblem',
- );
- $markup_offers = array();
- foreach ( $order->get_items() as $item ) {
- if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
- continue;
- }
- $product = $order->get_product_from_item( $item );
- $product_exists = is_object( $product );
- $is_visible = $product_exists && $product->is_visible();
- $markup_offers[] = array(
- '@type' => 'Offer',
- 'price' => $order->get_line_subtotal( $item ),
- 'priceCurrency' => $order->get_currency(),
- 'priceSpecification' => array(
- 'price' => $order->get_line_subtotal( $item ),
- 'priceCurrency' => $order->get_currency(),
- 'eligibleQuantity' => array(
- '@type' => 'QuantitativeValue',
- 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),
- ),
- ),
- 'itemOffered' => array(
- '@type' => 'Product',
- 'name' => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),
- 'sku' => $product_exists ? $product->get_sku() : '',
- 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
- 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
- ),
- 'seller' => array(
- '@type' => 'Organization',
- 'name' => $shop_name,
- 'url' => $shop_url,
- ),
- );
- }
- $markup = array();
- $markup['@type'] = 'Order';
- $markup['url'] = $order_url;
- $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : '';
- $markup['orderNumber'] = $order->get_order_number();
- $markup['orderDate'] = $order->get_date_created()->format( 'c' );
- $markup['acceptedOffer'] = $markup_offers;
- $markup['discount'] = $order->get_total_discount();
- $markup['discountCurrency'] = $order->get_currency();
- $markup['price'] = $order->get_total();
- $markup['priceCurrency'] = $order->get_currency();
- $markup['priceSpecification'] = array(
- 'price' => $order->get_total(),
- 'priceCurrency' => $order->get_currency(),
- 'valueAddedTaxIncluded' => 'true',
- );
- $markup['billingAddress'] = array(
- '@type' => 'PostalAddress',
- 'name' => $order->get_formatted_billing_full_name(),
- 'streetAddress' => $order->get_billing_address_1(),
- 'postalCode' => $order->get_billing_postcode(),
- 'addressLocality' => $order->get_billing_city(),
- 'addressRegion' => $order->get_billing_state(),
- 'addressCountry' => $order->get_billing_country(),
- 'email' => $order->get_billing_email(),
- 'telephone' => $order->get_billing_phone(),
- );
- $markup['customer'] = array(
- '@type' => 'Person',
- 'name' => $order->get_formatted_billing_full_name(),
- );
- $markup['merchant'] = array(
- '@type' => 'Organization',
- 'name' => $shop_name,
- 'url' => $shop_url,
- );
- $markup['potentialAction'] = array(
- '@type' => 'ViewAction',
- 'name' => 'View Order',
- 'url' => $order_url,
- 'target' => $order_url,
- );
- $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );
- }
- }
|