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.' );
}
/**