Commit a5b92306a20 for woocommerce
commit a5b92306a201ea01350b786800dc1de07585dde2
Author: Bero <1199991+bgrgicak@users.noreply.github.com>
Date: Thu Nov 5 08:31:59 2026 +0100
Show the store return policy on product pages (#70619)
* Show the store return policy on product pages
* Add changelog entry for return policy schema
diff --git a/plugins/woocommerce/changelog/add-product-return-policy-schema b/plugins/woocommerce/changelog/add-product-return-policy-schema
new file mode 100644
index 00000000000..7c0ac31eb1a
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-product-return-policy-schema
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Make the store return policy available to agents on product pages.
diff --git a/plugins/woocommerce/includes/class-wc-structured-data.php b/plugins/woocommerce/includes/class-wc-structured-data.php
index 4cc02958fe2..713dbb94942 100644
--- a/plugins/woocommerce/includes/class-wc-structured-data.php
+++ b/plugins/woocommerce/includes/class-wc-structured-data.php
@@ -158,7 +158,7 @@ class WC_Structured_Data {
$types = array();
$types[] = is_shop() || is_product_category() || is_product() ? 'product' : '';
$types[] = is_shop() && is_front_page() ? 'website' : '';
- $types[] = $policy_page_id > 0 && is_page( $policy_page_id ) && ! doing_action( 'woocommerce_email_order_details' ) ? 'onlinestore' : '';
+ $types[] = $policy_page_id > 0 && ( is_page( $policy_page_id ) || is_product() ) && ! doing_action( 'woocommerce_email_order_details' ) ? 'onlinestore' : '';
$types[] = is_product() ? 'review' : '';
$types[] = 'breadcrumblist';
$types[] = 'order';
@@ -706,7 +706,7 @@ class WC_Structured_Data {
}
/**
- * Generates link-only return-policy data on the selected public policy page.
+ * Generates link-only return-policy data on product pages and the selected public policy page.
*
* Hooked into `wp_footer` before structured data is output, so it cannot leak into order emails.
*
@@ -714,7 +714,7 @@ class WC_Structured_Data {
*/
public function generate_online_store_data(): void {
$page_id = wc_get_page_id( 'refund_returns' );
- if ( $page_id <= 0 || ! is_page( $page_id ) ) {
+ if ( $page_id <= 0 || ( ! is_page( $page_id ) && ! is_product() ) ) {
return;
}
@@ -739,7 +739,7 @@ class WC_Structured_Data {
);
/**
- * Filters the store and return-policy structured data on the policy page.
+ * Filters the store and return-policy structured data.
*
* @since 11.3.0
* @param array $markup Structured data for the online store.
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php b/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
index 3a9160e68d0..adce45e8099 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-structured-data-test.php
@@ -1103,6 +1103,92 @@ class WC_Structured_Data_Test extends \WC_Unit_Test_Case {
$this->assertStringNotContainsString( 'OnlineStore', $email, 'The policy node must not appear in an order email, even after the footer.' );
}
+ /**
+ * @testdox Product pages output the store return-policy link only while the selected page is public.
+ */
+ public function test_product_page_outputs_online_store_return_policy(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $page_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ )
+ );
+ update_option( 'woocommerce_refund_returns_page_id', $page_id );
+ $this->go_to( get_permalink( $product->get_id() ) );
+
+ $filtered_page = null;
+ add_filter(
+ 'woocommerce_structured_data_online_store',
+ static function ( $markup, $page ) use ( &$filtered_page ) {
+ $filtered_page = $page;
+ $markup['name'] = 'Filtered store';
+ return $markup;
+ },
+ 10,
+ 2
+ );
+ $this->structured_data->generate_product_data( $product );
+ $this->structured_data->generate_online_store_data();
+ ob_start();
+ $this->structured_data->output_structured_data();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( '"@type":"Product"', $output );
+ $this->assertStringContainsString( '"@type":"OnlineStore"', $output );
+ $this->assertStringContainsString( '"merchantReturnLink":"' . get_permalink( $page_id ) . '"', $output );
+ $this->assertStringContainsString( '"name":"Filtered store"', $output );
+ $this->assertSame( $page_id, $filtered_page->ID );
+
+ remove_all_actions( 'woocommerce_email_order_details' );
+ add_action( 'woocommerce_email_order_details', array( $this->structured_data, 'output_email_structured_data' ), 30, 3 );
+ ob_start();
+ do_action( 'woocommerce_email_order_details', null, false, false );
+ $this->assertStringNotContainsString( 'OnlineStore', ob_get_clean(), 'Order emails must not include the store node.' );
+
+ foreach ( array( 'draft', 'private', 'trash' ) as $status ) {
+ wp_update_post(
+ array(
+ 'ID' => $page_id,
+ 'post_status' => $status,
+ )
+ );
+ $structured_data = new WC_Structured_Data();
+ $structured_data->generate_online_store_data();
+ $this->assertSame( array(), $structured_data->get_data(), "{$status} pages must not be advertised." );
+ }
+
+ wp_update_post(
+ array(
+ 'ID' => $page_id,
+ 'post_status' => 'publish',
+ 'post_password' => 'secret',
+ )
+ );
+ $structured_data = new WC_Structured_Data();
+ $structured_data->generate_online_store_data();
+ $this->assertSame( array(), $structured_data->get_data(), 'Password-protected pages must not be advertised.' );
+
+ wp_update_post(
+ array(
+ 'ID' => $page_id,
+ 'post_password' => '',
+ )
+ );
+ $invalid_url = static function () {
+ return 'javascript:alert(1)';
+ };
+ add_filter( 'woocommerce_get_refund_returns_page_permalink', $invalid_url );
+ $structured_data = new WC_Structured_Data();
+ $structured_data->generate_online_store_data();
+ $this->assertSame( array(), $structured_data->get_data(), 'Invalid policy URLs must not be advertised.' );
+ remove_filter( 'woocommerce_get_refund_returns_page_permalink', $invalid_url );
+
+ wp_delete_post( $page_id, true );
+ $structured_data = new WC_Structured_Data();
+ $structured_data->generate_online_store_data();
+ $this->assertSame( array(), $structured_data->get_data(), 'Deleted pages must not be advertised.' );
+ }
+
/**
* @testdox No return-policy data is generated on another page or for an unpublished, private, or password-protected page.
*/