WC_Structured_Data

Structured data's handler and generator using JSON-LD format.

Defined (1)

The class is defined in the following location(s).

/includes/class-wc-structured-data.php  
  1. class WC_Structured_Data { 
  2.  
  3. /** 
  4. * @var array $_data 
  5. */ 
  6. private $_data = array(); 
  7.  
  8. /** 
  9. * Constructor. 
  10. */ 
  11. public function __construct() { 
  12. // Generate structured data. 
  13. add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 ); 
  14. add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 ); 
  15. add_action( 'woocommerce_shop_loop', array( $this, 'generate_product_data' ), 10 ); 
  16. add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 ); 
  17. add_action( 'woocommerce_review_meta', array( $this, 'generate_review_data' ), 20 ); 
  18. add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 ); 
  19.  
  20. // Output structured data. 
  21. add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 ); 
  22. add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 ); 
  23.  
  24. /** 
  25. * Sets data. 
  26. * @param array $data Structured data. 
  27. * @param bool $reset Unset data (default: false). 
  28. * @return bool 
  29. */ 
  30. public function set_data( $data, $reset = false ) { 
  31. if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1, 20}$|', $data['@type'] ) ) { 
  32. return false; 
  33.  
  34. if ( $reset && isset( $this->_data ) ) { 
  35. unset( $this->_data ); 
  36.  
  37. $this->_data[] = $data; 
  38.  
  39. return true; 
  40.  
  41. /** 
  42. * Gets data. 
  43. * @return array 
  44. */ 
  45. public function get_data() { 
  46. return $this->_data; 
  47.  
  48. /** 
  49. * Structures and returns data. 
  50. * List of types available by default for specific request: 
  51. * 'product',  
  52. * 'review',  
  53. * 'breadcrumblist',  
  54. * 'website',  
  55. * 'order',  
  56. * @param array $types Structured data types. 
  57. * @return array 
  58. */ 
  59. public function get_structured_data( $types ) { 
  60. $data = array(); 
  61.  
  62. // Put together the values of same type of structured data. 
  63. foreach ( $this->get_data() as $value ) { 
  64. $data[ strtolower( $value['@type'] ) ][] = $value; 
  65.  
  66. // Wrap the multiple values of each type inside a graph... Then add context to each type. 
  67. foreach ( $data as $type => $value ) { 
  68. $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; 
  69. $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; 
  70.  
  71. // If requested types, pick them up... Finally change the associative array to an indexed one. 
  72. $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); 
  73.  
  74. if ( ! empty( $data ) ) { 
  75. $data = count( $data ) > 1 ? array( '@graph' => $data ) : $data[0]; 
  76.  
  77. return $data; 
  78.  
  79. /** 
  80. * Get data types for pages. 
  81. * @return array 
  82. */ 
  83. protected function get_data_type_for_page() { 
  84. $types = array(); 
  85. $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; 
  86. $types[] = is_shop() && is_front_page() ? 'website' : ''; 
  87. $types[] = is_product() ? 'review' : ''; 
  88. $types[] = ! is_shop() ? 'breadcrumblist' : ''; 
  89. $types[] = 'order'; 
  90.  
  91. return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); 
  92.  
  93. /** 
  94. * Makes sure email structured data only outputs on non-plain text versions. 
  95. * @param WP_Order $order Order data. 
  96. * @param bool $sent_to_admin Send to admin (default: false). 
  97. * @param bool $plain_text Plain text email (default: false). 
  98. */ 
  99. public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { 
  100. if ( $plain_text ) { 
  101. return; 
  102. echo '<div style="display: none; font-size: 0; max-height: 0; line-height: 0; padding: 0; mso-hide: all;">'; 
  103. $this->output_structured_data(); 
  104. echo '</div>'; 
  105.  
  106. /** 
  107. * Sanitizes, encodes and outputs structured data. 
  108. * Hooked into `wp_footer` action hook. 
  109. * Hooked into `woocommerce_email_order_details` action hook. 
  110. */ 
  111. public function output_structured_data() { 
  112. $types = $this->get_data_type_for_page(); 
  113.  
  114. if ( $data = wc_clean( $this->get_structured_data( $types ) ) ) { 
  115. echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>'; 
  116.  
  117. /** 
  118. |-------------------------------------------------------------------------- 
  119. | Generators 
  120. |-------------------------------------------------------------------------- 
  121. | Methods for generating specific structured data types: 
  122. | - Product 
  123. | - Review 
  124. | - BreadcrumbList 
  125. | - WebSite 
  126. | - Order 
  127. | The generated data is stored into `$this->_data`. 
  128. | See the methods above for handling `$this->_data`. 
  129. */ 
  130.  
  131. /** 
  132. * Generates Product structured data. 
  133. * Hooked into `woocommerce_single_product_summary` action hook. 
  134. * Hooked into `woocommerce_shop_loop` action hook. 
  135. * @param WC_Product $product Product data (default: null). 
  136. */ 
  137. public function generate_product_data( $product = null ) { 
  138. if ( ! is_object( $product ) ) { 
  139. global $product; 
  140.  
  141. if ( ! is_a( $product, 'WC_Product' ) ) { 
  142. return; 
  143.  
  144. $shop_name = get_bloginfo( 'name' ); 
  145. $shop_url = home_url(); 
  146. $currency = get_woocommerce_currency(); 
  147. $markup = array(); 
  148. $markup['@type'] = 'Product'; 
  149. $markup['@id'] = get_permalink( $product->get_id() ); 
  150. $markup['url'] = $markup['@id']; 
  151. $markup['name'] = $product->get_name(); 
  152.  
  153. if ( apply_filters( 'woocommerce_structured_data_product_limit', is_product_taxonomy() || is_shop() ) ) { 
  154. $this->set_data( apply_filters( 'woocommerce_structured_data_product_limited', $markup, $product ) ); 
  155. return; 
  156.  
  157. if ( '' !== $product->get_price() ) { 
  158. $markup_offer = array( 
  159. '@type' => 'Offer',  
  160. 'priceCurrency' => $currency,  
  161. 'availability' => 'https://schema.org/' . $stock = ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ),  
  162. 'sku' => $product->get_sku(),  
  163. 'image' => wp_get_attachment_url( $product->get_image_id() ),  
  164. 'description' => $product->get_description(),  
  165. 'seller' => array( 
  166. '@type' => 'Organization',  
  167. 'name' => $shop_name,  
  168. 'url' => $shop_url,  
  169. ),  
  170. ); 
  171.  
  172. if ( $product->is_type( 'variable' ) ) { 
  173. $prices = $product->get_variation_prices(); 
  174.  
  175. $markup_offer['priceSpecification'] = array( 
  176. 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),  
  177. 'minPrice' => wc_format_decimal( current( $prices['price'] ), wc_get_price_decimals() ),  
  178. 'maxPrice' => wc_format_decimal( end( $prices['price'] ), wc_get_price_decimals() ),  
  179. 'priceCurrency' => $currency,  
  180. ); 
  181. } else { 
  182. $markup_offer['price'] = wc_format_decimal( $product->get_price(), wc_get_price_decimals() ); 
  183.  
  184. $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); 
  185.  
  186. if ( $product->get_rating_count() ) { 
  187. $markup['aggregateRating'] = array( 
  188. '@type' => 'AggregateRating',  
  189. 'ratingValue' => $product->get_average_rating(),  
  190. 'ratingCount' => $product->get_rating_count(),  
  191. 'reviewCount' => $product->get_review_count(),  
  192. ); 
  193.  
  194. $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); 
  195.  
  196. /** 
  197. * Generates Review structured data. 
  198. * Hooked into `woocommerce_review_meta` action hook. 
  199. * @param WP_Comment $comment Comment data. 
  200. */ 
  201. public function generate_review_data( $comment ) { 
  202. $markup = array(); 
  203. $markup['@type'] = 'Review'; 
  204. $markup['@id'] = get_comment_link( $comment->comment_ID ); 
  205. $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); 
  206. $markup['description'] = get_comment_text( $comment->comment_ID ); 
  207. $markup['itemReviewed'] = array( 
  208. '@type' => 'Product',  
  209. 'name' => get_the_title( $comment->post_ID ),  
  210. ); 
  211. if ( $rating = get_comment_meta( $comment->comment_ID, 'rating', true ) ) { 
  212. $markup['reviewRating'] = array( 
  213. '@type' => 'rating',  
  214. 'ratingValue' => $rating,  
  215. ); 
  216.  
  217. // Skip replies unless they have a rating. 
  218. } elseif ( $comment->comment_parent ) { 
  219. return; 
  220.  
  221. $markup['author'] = array( 
  222. '@type' => 'Person',  
  223. 'name' => get_comment_author( $comment->comment_ID ),  
  224. ); 
  225.  
  226. $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); 
  227.  
  228. /** 
  229. * Generates BreadcrumbList structured data. 
  230. * Hooked into `woocommerce_breadcrumb` action hook. 
  231. * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. 
  232. */ 
  233. public function generate_breadcrumblist_data( $breadcrumbs ) { 
  234. $crumbs = $breadcrumbs->get_breadcrumb(); 
  235.  
  236. if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { 
  237. return; 
  238.  
  239. $markup = array(); 
  240. $markup['@type'] = 'BreadcrumbList'; 
  241. $markup['itemListElement'] = array(); 
  242.  
  243. foreach ( $crumbs as $key => $crumb ) { 
  244. $markup['itemListElement'][ $key ] = array( 
  245. '@type' => 'ListItem',  
  246. 'position' => $key + 1,  
  247. 'item' => array( 
  248. 'name' => $crumb[0],  
  249. ),  
  250. ); 
  251.  
  252. if ( ! empty( $crumb[1] ) && sizeof( $crumbs ) !== $key + 1 ) { 
  253. $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); 
  254.  
  255. $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); 
  256.  
  257. /** 
  258. * Generates WebSite structured data. 
  259. * Hooked into `woocommerce_before_main_content` action hook. 
  260. */ 
  261. public function generate_website_data() { 
  262. $markup = array(); 
  263. $markup['@type'] = 'WebSite'; 
  264. $markup['name'] = get_bloginfo( 'name' ); 
  265. $markup['url'] = home_url(); 
  266. $markup['potentialAction'] = array( 
  267. '@type' => 'SearchAction',  
  268. 'target' => home_url( '?s={search_term_string}&post_type=product' ),  
  269. 'query-input' => 'required name=search_term_string',  
  270. ); 
  271.  
  272. $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); 
  273.  
  274. /** 
  275. * Generates Order structured data. 
  276. * Hooked into `woocommerce_email_order_details` action hook. 
  277. * @param WP_Order $order Order data. 
  278. * @param bool $sent_to_admin Send to admin (default: false). 
  279. * @param bool $plain_text Plain text email (default: false). 
  280. */ 
  281. public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { 
  282. if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { 
  283. return; 
  284.  
  285. $shop_name = get_bloginfo( 'name' ); 
  286. $shop_url = home_url(); 
  287. $order_url = $sent_to_admin ? admin_url( 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' ) : $order->get_view_order_url(); 
  288. $order_statuses = array( 
  289. 'pending' => 'https://schema.org/OrderPaymentDue',  
  290. 'processing' => 'https://schema.org/OrderProcessing',  
  291. 'on-hold' => 'https://schema.org/OrderProblem',  
  292. 'completed' => 'https://schema.org/OrderDelivered',  
  293. 'cancelled' => 'https://schema.org/OrderCancelled',  
  294. 'refunded' => 'https://schema.org/OrderReturned',  
  295. 'failed' => 'https://schema.org/OrderProblem',  
  296. ); 
  297.  
  298. $markup_offers = array(); 
  299. foreach ( $order->get_items() as $item ) { 
  300. if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { 
  301. continue; 
  302.  
  303. $product = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item ); 
  304. $product_exists = is_object( $product ); 
  305. $is_visible = $product_exists && $product->is_visible(); 
  306.  
  307. $markup_offers[] = array( 
  308. '@type' => 'Offer',  
  309. 'price' => $order->get_line_subtotal( $item ),  
  310. 'priceCurrency' => $order->get_currency(),  
  311. 'priceSpecification' => array( 
  312. 'price' => $order->get_line_subtotal( $item ),  
  313. 'priceCurrency' => $order->get_currency(),  
  314. 'eligibleQuantity' => array( 
  315. '@type' => 'QuantitativeValue',  
  316. 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),  
  317. ),  
  318. ),  
  319. 'itemOffered' => array( 
  320. '@type' => 'Product',  
  321. 'name' => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),  
  322. 'sku' => $product_exists ? $product->get_sku() : '',  
  323. 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',  
  324. 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),  
  325. ),  
  326. 'seller' => array( 
  327. '@type' => 'Organization',  
  328. 'name' => $shop_name,  
  329. 'url' => $shop_url,  
  330. ),  
  331. ); 
  332.  
  333. $markup = array(); 
  334. $markup['@type'] = 'Order'; 
  335. $markup['url'] = $order_url; 
  336. $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; 
  337. $markup['orderNumber'] = $order->get_order_number(); 
  338. $markup['orderDate'] = $order->get_date_created()->format( 'c' ); 
  339. $markup['acceptedOffer'] = $markup_offers; 
  340. $markup['discount'] = $order->get_total_discount(); 
  341. $markup['discountCurrency'] = $order->get_currency(); 
  342. $markup['price'] = $order->get_total(); 
  343. $markup['priceCurrency'] = $order->get_currency(); 
  344. $markup['priceSpecification'] = array( 
  345. 'price' => $order->get_total(),  
  346. 'priceCurrency' => $order->get_currency(),  
  347. 'valueAddedTaxIncluded' => true,  
  348. ); 
  349. $markup['billingAddress'] = array( 
  350. '@type' => 'PostalAddress',  
  351. 'name' => $order->get_formatted_billing_full_name(),  
  352. 'streetAddress' => $order->get_billing_address_1(),  
  353. 'postalCode' => $order->get_billing_postcode(),  
  354. 'addressLocality' => $order->get_billing_city(),  
  355. 'addressRegion' => $order->get_billing_state(),  
  356. 'addressCountry' => $order->get_billing_country(),  
  357. 'email' => $order->get_billing_email(),  
  358. 'telephone' => $order->get_billing_phone(),  
  359. ); 
  360. $markup['customer'] = array( 
  361. '@type' => 'Person',  
  362. 'name' => $order->get_formatted_billing_full_name(),  
  363. ); 
  364. $markup['merchant'] = array( 
  365. '@type' => 'Organization',  
  366. 'name' => $shop_name,  
  367. 'url' => $shop_url,  
  368. ); 
  369. $markup['potentialAction'] = array( 
  370. '@type' => 'ViewAction',  
  371. 'name' => 'View Order',  
  372. 'url' => $order_url,  
  373. 'target' => $order_url,  
  374. ); 
  375.  
  376. $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );