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"]