Commit bc22f297b4e for woocommerce

commit bc22f297b4e6d29bbe0cc5d43bf6205c6d4491ea
Author: Bero <1199991+bgrgicak@users.noreply.github.com>
Date:   Tue Nov 3 13:27:57 2026 +0100

    Add Open Graph metadata to product archives (#70325)

    * Add Open Graph metadata to the shop page

    * Add Open Graph metadata to product archives

    * 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-archive-open-graph b/plugins/woocommerce/changelog/add-product-archive-open-graph
new file mode 100644
index 00000000000..d2a8fe1a0a7
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-product-archive-open-graph
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Improve shared links to product categories, brands, and tags.
diff --git a/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphUrl.php b/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphUrl.php
new file mode 100644
index 00000000000..78d9ef2c818
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StructuredData/CatalogOpenGraphUrl.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Catalog Open Graph URLs.
+ *
+ * @package WooCommerce
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StructuredData;
+
+use Automattic\WooCommerce\Blocks\Utils\Utils;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds the current catalog URL for Open Graph metadata.
+ *
+ * @internal
+ */
+class CatalogOpenGraphUrl {
+	/**
+	 * Get the current URL without the add-to-cart action or private share key.
+	 *
+	 * @since 11.4.0
+	 * @return string
+	 */
+	public function get_url(): string {
+		$url   = Utils::get_current_page_url();
+		$parts = explode( '?', $url, 2 );
+		if ( ! isset( $parts[1] ) ) {
+			return $url;
+		}
+		$params = array_filter(
+			explode( '&', $parts[1] ),
+			static function ( $param ) {
+				return ! in_array( urldecode( explode( '=', $param, 2 )[0] ), array( 'add-to-cart', 'woo-share' ), true );
+			}
+		);
+		return $parts[0] . ( $params ? '?' . implode( '&', $params ) : '' );
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php b/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
index f83b89ff385..0dfee6cf21e 100644
--- a/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
+++ b/plugins/woocommerce/src/Internal/StructuredData/OpenGraph.php
@@ -82,7 +82,7 @@ class OpenGraph {
 		$this->default_card = array_column( $tags, 'twitter:card' )[0] ?? null;

 		/**
-		 * Filters Open Graph metadata before output. Applied to product and shop pages.
+		 * Filters Open Graph metadata before output. Applied to product, shop and product taxonomy pages.
 		 * Other page types may be supported later; callbacks must check the object type.
 		 * The first tag in each group is its root; details follow it. Remove or replace an entire group to omit or replace its details.
 		 *
@@ -145,6 +145,16 @@ class OpenGraph {
 			return $this->get_shop_page_data();
 		}

+		if ( ! is_search() && is_tax( array( 'product_cat', 'product_brand', 'product_tag' ) ) ) {
+			$term = get_queried_object();
+			if ( $term instanceof \WP_Term ) {
+				return array(
+					'tags'    => wc_get_container()->get( ProductTaxonomyOpenGraphTags::class )->get_tags( $term ),
+					'context' => $term,
+				);
+			}
+		}
+
 		return null;
 	}

diff --git a/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php b/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php
new file mode 100644
index 00000000000..6d427275f18
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StructuredData/ProductTaxonomyOpenGraphTags.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Product taxonomy Open Graph metadata.
+ *
+ * @package WooCommerce
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StructuredData;
+
+use WP_Term;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Builds Open Graph metadata for product taxonomy archives.
+ *
+ * @internal
+ */
+class ProductTaxonomyOpenGraphTags {
+	/**
+	 * Build metadata for a product category, brand or tag.
+	 *
+	 * @since 11.4.0
+	 * @param WP_Term $term Current product taxonomy term.
+	 * @return array<array<string, string>>
+	 */
+	public function get_tags( WP_Term $term ): array {
+		$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;
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/StructuredData/OpenGraphTest.php b/plugins/woocommerce/tests/php/src/Internal/StructuredData/OpenGraphTest.php
index 3695e931b2c..a9964e1280f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StructuredData/OpenGraphTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StructuredData/OpenGraphTest.php
@@ -356,7 +356,7 @@ class OpenGraphTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox Should add shop metadata and preserve a provider's image.
+	 * @testdox Should add shop and category metadata and preserve a provider's image.
 	 */
 	public function test_shop_metadata(): void {
 		$shop_id = self::factory()->post->create(
@@ -392,6 +392,50 @@ class OpenGraphTest extends WC_Unit_Test_Case {
 		$this->assertSame( array( '600' ), $tags['og:image:width'] );
 		$this->assertArrayNotHasKey( 'og:image:height', $tags );
 		$this->assertSame( array( 'summary_large_image' ), $tags['twitter:card'] );
+
+		$term_id = self::factory()->term->create(
+			array(
+				'taxonomy' => 'product_cat',
+				'name'     => 'Coffee beans',
+			)
+		);
+		wp_set_object_terms( $this->product->get_id(), $term_id, 'product_cat' );
+		$term = get_term( $term_id, 'product_cat' );
+		$this->go_to( get_term_link( $term ) );
+		$context = null;
+		add_filter(
+			'woocommerce_open_graph_tags',
+			static function ( $tags, $source ) use ( &$context ) {
+				$context = $source;
+				return $tags;
+			},
+			10,
+			2
+		);
+
+		$tags = $this->render_meta_tags( '' );
+		$this->assertSame( array( 'Coffee beans' ), $tags['og:title'] ?? array(), 'The category archive should use the category name.' );
+		$this->assertInstanceOf( \WP_Term::class, $context, 'The filter should receive a term.' );
+		$this->assertSame( $term_id, $context->term_id, 'The filter should receive the queried category.' );
+	}
+
+	/**
+	 * @testdox Should use the category name as the preview title on paginated archives.
+	 */
+	public function test_paginated_category_title(): void {
+		$term_id = self::factory()->term->create(
+			array(
+				'taxonomy' => 'product_cat',
+				'name'     => 'Coffee beans',
+			)
+		);
+		wp_set_object_terms( $this->product->get_id(), $term_id, 'product_cat' );
+		$this->go_to( get_term_link( $term_id, 'product_cat' ) );
+		set_query_var( 'paged', 2 );
+
+		$tags = $this->render_meta_tags( '' );
+
+		$this->assertSame( array( 'Coffee beans' ), $tags['og:title'] ?? array(), 'The title should not include the page number.' );
 	}

 	/**