Commit bce757f34e8 for woocommerce

commit bce757f34e8cd1440a5b930f6e6069d8410e326c
Author: Bero <1199991+bgrgicak@users.noreply.github.com>
Date:   Tue Nov 3 14:55:59 2026 +0100

    Add Open Graph metadata to product search (#70327)

    * Add Open Graph metadata to the shop page

    * Add Open Graph metadata to product archives

    * Add Open Graph metadata to product search

    * Exclude private share key from catalog metadata

    * Remove page numbers from taxonomy preview titles

    * Add category archive coverage to Open Graph test

    * Test paginated category preview title

diff --git a/plugins/woocommerce/changelog/add-product-search-open-graph b/plugins/woocommerce/changelog/add-product-search-open-graph
new file mode 100644
index 00000000000..ed7693a673e
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-product-search-open-graph
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Improve shared links to product search results.
diff --git a/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphTags.php b/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphTags.php
new file mode 100644
index 00000000000..c4a574f066d
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphTags.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Shared catalog Open Graph metadata.
+ *
+ * @package WooCommerce
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StructuredData;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds metadata shared by product archives and search results.
+ *
+ * @internal
+ */
+class CatalogOpenGraphTags {
+	/**
+	 * Build metadata from a title, URL and optional term details.
+	 *
+	 * @since 11.4.0
+	 * @param string $title Page title.
+	 * @param string $url Canonical page URL.
+	 * @param string $description Term description, when available.
+	 * @param mixed  $term_image_id Term image ID, when available.
+	 * @return array<array<string, string>>
+	 */
+	public function get_tags( string $title, string $url, string $description = '', $term_image_id = 0 ): array {
+		$shop = get_post( wc_get_page_id( 'shop' ) );
+		if ( '' === trim( wp_strip_all_tags( strip_shortcodes( $description ) ) ) && $shop instanceof \WP_Post && 'publish' === $shop->post_status && '' === $shop->post_password ) {
+			$description = $shop->post_excerpt;
+		}
+		if ( '' === trim( wp_strip_all_tags( strip_shortcodes( $description ) ) ) ) {
+			$description = get_bloginfo( 'description' );
+		}
+		$description = wc_get_container()->get( OpenGraphDescription::class )->clean( $description );
+		$tags        = array(
+			array( 'og:title' => $title ),
+			array( 'og:type' => 'website' ),
+			array( 'og:url' => $url ),
+			array( 'og:description' => wc_trim_string( $description ) ),
+			array( 'og:site_name' => get_bloginfo( 'name' ) ),
+		);
+		$locale      = get_locale();
+		if ( preg_match( '/^[a-z]{2,3}_[A-Z]{2}$/', $locale ) ) {
+			$tags[] = array( 'og:locale' => $locale );
+		}
+
+		$image_ids = array(
+			$term_image_id,
+			$shop instanceof \WP_Post && 'publish' === $shop->post_status && '' === $shop->post_password ? get_post_thumbnail_id( $shop ) : 0,
+			get_theme_mod( 'custom_logo' ),
+			get_option( 'site_icon' ),
+		);
+		$has_image = false;
+		foreach ( $image_ids as $image_id ) {
+			if ( ! is_numeric( $image_id ) || (int) $image_id <= 0 ) {
+				continue;
+			}
+			$image = wc_get_container()->get( OpenGraphImageTags::class )->get_tags( (int) $image_id, $title, true );
+			if ( ! $image ) {
+				continue;
+			}
+			$tags[]    = $image;
+			$has_image = true;
+			break;
+		}
+		$tags[] = array( 'twitter:card' => $has_image ? 'summary_large_image' : 'summary' );
+
+		return $tags;
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php b/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
index 0dfee6cf21e..8536576e285 100644
--- a/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
+++ b/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
@@ -82,8 +82,9 @@ class OpenGraph {
 		$this->default_card = array_column( $tags, 'twitter:card' )[0] ?? null;

 		/**
-		 * Filters Open Graph metadata before output. Applied to product, shop and product taxonomy pages.
+		 * Filters Open Graph metadata before output. Applied to product, shop, product taxonomy and product search pages.
 		 * Other page types may be supported later; callbacks must check the object type.
+		 * Product searches may use the shop page as their queried object; use is_search() to distinguish them.
 		 * The first tag in each group is its root; details follow it. Remove or replace an entire group to omit or replace its details.
 		 *
 		 * @since 11.4.0
@@ -136,6 +137,20 @@ class OpenGraph {
 			return $this->get_product_page_data();
 		}

+		if ( is_search() && is_post_type_archive( 'product' ) ) {
+			$search = get_search_query( false );
+			if ( is_string( $search ) && '' !== trim( $search ) ) {
+				$context = get_queried_object();
+				if ( ! $context instanceof \WP_Post && ! $context instanceof \WP_Term && ! $context instanceof \WP_Post_Type ) {
+					$context = null;
+				}
+				return array(
+					'tags'    => wc_get_container()->get( ProductSearchOpenGraphTags::class )->get_tags( $search ),
+					'context' => $context,
+				);
+			}
+		}
+
 		if (
 			is_shop()
 			&& ! is_search()
diff --git a/plugins/woocommerce/src/Internal/StructuredData/ProductSearchOpenGraphTags.php b/plugins/woocommerce/src/Internal/StructuredData/ProductSearchOpenGraphTags.php
new file mode 100644
index 00000000000..b7cedd92720
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StructuredData/ProductSearchOpenGraphTags.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Product search Open Graph metadata.
+ *
+ * @package WooCommerce
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StructuredData;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds Open Graph metadata for product search results.
+ *
+ * @internal
+ */
+class ProductSearchOpenGraphTags {
+	/**
+	 * Build metadata for a product search.
+	 *
+	 * @since 11.4.0
+	 * @param string $search Search query.
+	 * @return array<array<string, string>>
+	 */
+	public function get_tags( string $search ): array {
+		/* translators: %s: search query. */
+		$title = sprintf( __( 'Search results for “%s”', 'woocommerce' ), $search );
+		$page  = max( 1, (int) get_query_var( 'paged' ) );
+		if ( $page > 1 ) {
+			/* translators: 1: search title, 2: page number. */
+			$title = sprintf( __( '%1$s – Page %2$d', 'woocommerce' ), $title, $page );
+		}
+		$url = wc_get_container()->get( CatalogOpenGraphUrl::class )->get_url();
+
+		return wc_get_container()->get( CatalogOpenGraphTags::class )->get_tags( $title, $url );
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php b/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php
index 6d427275f18..d8ce80cd204 100644
--- a/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php
+++ b/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php
@@ -30,48 +30,6 @@ class ProductTaxonomyOpenGraphTags {
 		$title = $term->name;
 		$url   = wc_get_container()->get( CatalogOpenGraphUrl::class )->get_url();

-		$shop        = get_post( wc_get_page_id( 'shop' ) );
-		$description = $term->description;
-		if ( '' === trim( wp_strip_all_tags( strip_shortcodes( $description ) ) ) && $shop instanceof \WP_Post && 'publish' === $shop->post_status && '' === $shop->post_password ) {
-			$description = $shop->post_excerpt;
-		}
-		if ( '' === trim( wp_strip_all_tags( strip_shortcodes( $description ) ) ) ) {
-			$description = get_bloginfo( 'description' );
-		}
-		$description = wc_get_container()->get( OpenGraphDescription::class )->clean( $description );
-		$tags        = array(
-			array( 'og:title' => $title ),
-			array( 'og:type' => 'website' ),
-			array( 'og:url' => $url ),
-			array( 'og:description' => wc_trim_string( $description ) ),
-			array( 'og:site_name' => get_bloginfo( 'name' ) ),
-		);
-		$locale      = get_locale();
-		if ( preg_match( '/^[a-z]{2,3}_[A-Z]{2}$/', $locale ) ) {
-			$tags[] = array( 'og:locale' => $locale );
-		}
-
-		$image_ids = array(
-			get_term_meta( $term->term_id, 'thumbnail_id', true ),
-			$shop instanceof \WP_Post && 'publish' === $shop->post_status && '' === $shop->post_password ? get_post_thumbnail_id( $shop ) : 0,
-			get_theme_mod( 'custom_logo' ),
-			get_option( 'site_icon' ),
-		);
-		$has_image = false;
-		foreach ( $image_ids as $image_id ) {
-			if ( ! is_numeric( $image_id ) || (int) $image_id <= 0 ) {
-				continue;
-			}
-			$image = wc_get_container()->get( OpenGraphImageTags::class )->get_tags( (int) $image_id, $title, true );
-			if ( ! $image ) {
-				continue;
-			}
-			$tags[]    = $image;
-			$has_image = true;
-			break;
-		}
-		$tags[] = array( 'twitter:card' => $has_image ? 'summary_large_image' : 'summary' );
-
-		return $tags;
+		return wc_get_container()->get( CatalogOpenGraphTags::class )->get_tags( $title, $url, $term->description, get_term_meta( $term->term_id, 'thumbnail_id', true ) );
 	}
 }