Commit ab7b24ff897 for woocommerce
commit ab7b24ff897ead13a8ca5102ed9ab9d23ad81037
Author: Bero <1199991+bgrgicak@users.noreply.github.com>
Date: Thu Nov 5 11:51:13 2026 +0100
Add uploaded gallery videos to product structured data (#70587)
* Add uploaded gallery videos to product structured data
* Add changelog entry for product gallery video schema
* Include all usable gallery videos in product structured data
* Add product gallery video structured data test
diff --git a/plugins/woocommerce/changelog/add-product-gallery-video-schema b/plugins/woocommerce/changelog/add-product-gallery-video-schema
new file mode 100644
index 00000000000..eded5985058
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-product-gallery-video-schema
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Make uploaded product gallery videos discoverable in structured data.
diff --git a/plugins/woocommerce/includes/class-wc-structured-data.php b/plugins/woocommerce/includes/class-wc-structured-data.php
index fe064584c90..30232df1a68 100644
--- a/plugins/woocommerce/includes/class-wc-structured-data.php
+++ b/plugins/woocommerce/includes/class-wc-structured-data.php
@@ -12,6 +12,7 @@
* @version 3.0.0
*/
+use Automattic\WooCommerce\Internal\ProductGallery\ProductMediaGallery;
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductType;
@@ -635,9 +636,89 @@ class WC_Structured_Data {
$term = get_term( $term->parent, 'product_cat' );
}
}
+
+ $videos = $this->get_product_gallery_video_data( $product );
+ if ( $videos ) {
+ $markup['subjectOf'] = $videos;
+ }
+
$this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
}
+ /**
+ * Get structured data for usable uploaded gallery videos.
+ *
+ * @param WC_Product $product Product data.
+ * @return array Video data.
+ */
+ private function get_product_gallery_video_data( $product ) {
+ if ( ! ProductMediaGallery::is_feature_enabled() ) {
+ return array();
+ }
+
+ $videos = ProductMediaGallery::get_stored_video_gallery_items( $product );
+ if ( ! $videos ) {
+ return array();
+ }
+
+ $ids = array_filter( array_map( 'absint', array_merge( array_column( $videos, 'id' ), array_column( $videos, 'poster_id' ) ) ) );
+ if ( $ids ) {
+ // Prime caches to reduce future queries.
+ _prime_post_caches( array_unique( $ids ) );
+ }
+
+ $video_data = array();
+ foreach ( ProductMediaGallery::get_product_media_gallery_items( $product ) as $item ) {
+ if ( 'video' !== $item['media_type'] ) {
+ continue;
+ }
+
+ $id = (int) $item['id'];
+ $attachment = get_post( $id );
+ if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
+ continue;
+ }
+ $url = wp_get_attachment_url( $id );
+ $date = get_post_datetime( $attachment, 'date', 'gmt' );
+ $poster_id = (int) ( $item['poster_id'] ?? 0 );
+ $thumbnail = $poster_id ? wp_get_attachment_image_url( $poster_id, 'full' ) : false;
+ if ( ! $thumbnail && $product->get_image_id() ) {
+ $thumbnail = wp_get_attachment_image_url( (int) $product->get_image_id(), 'full' );
+ }
+ $name = trim( wp_strip_all_tags( $attachment->post_title ) );
+ $name = $name ? $name : trim( wp_strip_all_tags( $product->get_name() ) );
+ if ( ! $url || ! $thumbnail || ! $date || ! $name ) {
+ continue;
+ }
+
+ $video = array(
+ '@type' => 'VideoObject',
+ 'name' => $name,
+ 'contentUrl' => $url,
+ 'thumbnailUrl' => $thumbnail,
+ 'uploadDate' => $date->format( DATE_ATOM ),
+ );
+ $description = trim( wp_strip_all_tags( $attachment->post_content ) );
+ if ( $description ) {
+ $video['description'] = $description;
+ }
+ /**
+ * Video attachment metadata.
+ *
+ * @var array<string, mixed>|false $metadata
+ */
+ $metadata = wp_get_attachment_metadata( $id );
+ $length = is_array( $metadata ) ? ( $metadata['length'] ?? null ) : null;
+ if ( is_int( $length ) && 0 < $length ) {
+ $video['duration'] = 'PT' . (int) $length . 'S';
+ }
+ $video_data[] = $video;
+ }
+
+ return $video_data;
+ }
+
+
/**
* Generates Review structured data.
*
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 e3f79598c75..c62207434ee 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
@@ -1,6 +1,8 @@
<?php
declare( strict_types = 1 );
+use Automattic\WooCommerce\Internal\ProductGallery\ProductMediaGallery;
+
/**
* Class WC_Structured_Data_Test.
*/
@@ -349,6 +351,91 @@ class WC_Structured_Data_Test extends \WC_Unit_Test_Case {
$this->assertEquals( $this->structured_data->prepare_gtin( '123.4e-5' ), '12345' );
}
+ /**
+ * @testdox Product schema includes usable gallery videos in order and omits them when disabled or without thumbnails.
+ */
+ public function test_product_gallery_video_structured_data(): void {
+ update_option( ProductMediaGallery::ENABLE_OPTION_NAME, 'yes' );
+ $uploads = wp_get_upload_dir();
+ $image = wp_insert_attachment(
+ array( 'post_mime_type' => 'image/jpeg' ),
+ $uploads['basedir'] . '/product.jpg'
+ );
+ $poster = wp_insert_attachment(
+ array( 'post_mime_type' => 'image/jpeg' ),
+ $uploads['basedir'] . '/poster.jpg'
+ );
+ $first = wp_insert_attachment(
+ array(
+ 'post_title' => 'First video',
+ 'post_mime_type' => 'video/mp4',
+ 'post_date' => '2024-01-02 03:04:05',
+ 'post_date_gmt' => '2024-01-02 03:04:05',
+ ),
+ $uploads['basedir'] . '/first.mp4'
+ );
+ $second = wp_insert_attachment(
+ array(
+ 'post_title' => 'Second video',
+ 'post_mime_type' => 'video/mp4',
+ 'post_date' => '2024-02-03 04:05:06',
+ 'post_date_gmt' => '2024-02-03 04:05:06',
+ ),
+ $uploads['basedir'] . '/second.mp4'
+ );
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_image_id( $image );
+ $product->save();
+ ProductMediaGallery::set_stored_video_gallery_items(
+ $product,
+ array(
+ array(
+ 'id' => $second,
+ 'position' => 1,
+ ),
+ array(
+ 'id' => $first,
+ 'position' => 0,
+ 'poster_id' => $poster,
+ ),
+ )
+ );
+
+ $sut = new WC_Structured_Data();
+ $sut->generate_product_data( $product );
+ $this->assertSame(
+ array(
+ array(
+ '@type' => 'VideoObject',
+ 'name' => 'First video',
+ 'contentUrl' => wp_get_attachment_url( $first ),
+ 'thumbnailUrl' => wp_get_attachment_image_url( $poster, 'full' ),
+ 'uploadDate' => '2024-01-02T03:04:05+00:00',
+ ),
+ array(
+ '@type' => 'VideoObject',
+ 'name' => 'Second video',
+ 'contentUrl' => wp_get_attachment_url( $second ),
+ 'thumbnailUrl' => wp_get_attachment_image_url( $image, 'full' ),
+ 'uploadDate' => '2024-02-03T04:05:06+00:00',
+ ),
+ ),
+ $sut->get_data()[0]['subjectOf']
+ );
+
+ update_option( ProductMediaGallery::ENABLE_OPTION_NAME, 'no' );
+ $sut = new WC_Structured_Data();
+ $sut->generate_product_data( $product );
+ $this->assertArrayNotHasKey( 'subjectOf', $sut->get_data()[0] );
+
+ update_option( ProductMediaGallery::ENABLE_OPTION_NAME, 'yes' );
+ $product->set_image_id( 0 );
+ $product->save();
+ $sut = new WC_Structured_Data();
+ $sut->generate_product_data( $product );
+ $this->assertSame( array( 'First video' ), array_column( $sut->get_data()[0]['subjectOf'], 'name' ) );
+ }
+
/**
* @testdox Product schema includes zero MPNs, omits empty values, and preserves extension filters.
* @testWith ["0"]