BreadcrumbList schema or empty array. */ public function build_breadcrumb( \WC_Product $product ): array { $terms = $this->get_product_categories( $product ); if ( empty( $terms ) ) { return array(); } $items = array(); $pos = 1; // Home as first breadcrumb item. $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => get_bloginfo( 'name' ), 'item' => home_url( '/' ), ); // Shop page. $shop_page_id = wc_get_page_id( 'shop' ); if ( $shop_page_id > 0 ) { $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => get_the_title( $shop_page_id ), 'item' => get_permalink( $shop_page_id ), ); } // Category hierarchy (deepest first in $terms, we want ancestors first). foreach ( $terms as $term ) { $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => $term->name, 'item' => get_term_link( $term ), ); } // Product itself as the final breadcrumb. $items[] = array( '@type' => 'ListItem', 'position' => $pos, 'name' => $product->get_name(), 'item' => get_permalink( $product->get_id() ), ); $breadcrumb = array( '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $items, ); /** * Filters the BreadcrumbList schema. * * @param array $breadcrumb The BreadcrumbList data. * @param \WC_Product $product The WooCommerce product. */ return apply_filters( 'aicommerce_breadcrumb_schema', $breadcrumb, $product ); } /** * Build Brand → Organization entity chain. * * @param \WC_Product $product The WooCommerce product. * * @return array Brand schema or empty array. */ public function build_brand( \WC_Product $product ): array { $brand_name = $product->get_meta( '_aicommerce_brand' ); if ( empty( $brand_name ) ) { // Fall back to store name as brand. $brand_name = get_option( 'aicommerce_default_brand', '' ); } if ( empty( $brand_name ) ) { return array(); } $brand = array( '@type' => 'Brand', 'name' => $brand_name, ); // If brand URL is available, add it. $brand_url = $product->get_meta( '_aicommerce_brand_url' ); if ( ! empty( $brand_url ) ) { $brand['url'] = $brand_url; } return $brand; } /** * Build Organization schema for the store. * * @return array Organization schema. */ public function build_organization(): array { $org = array( '@context' => 'https://schema.org', '@type' => 'Organization', 'name' => get_bloginfo( 'name' ), 'url' => home_url( '/' ), ); // Logo. $logo_id = get_theme_mod( 'custom_logo' ); if ( $logo_id ) { $logo_url = wp_get_attachment_url( $logo_id ); if ( $logo_url ) { $org['logo'] = $logo_url; } } // SameAs links from plugin settings. $same_as = $this->get_same_as_links(); if ( ! empty( $same_as ) ) { $org['sameAs'] = $same_as; } /** * Filters the Organization schema. * * @param array $org Organization schema data. */ return apply_filters( 'aicommerce_organization_schema', $org ); } /** * Get SameAs social/authority links from plugin settings. * * @return array URLs for SameAs property. */ private function get_same_as_links(): array { $settings = get_option( 'aicommerce_same_as_links', array() ); if ( ! is_array( $settings ) ) { return array(); } // Filter out empty values and ensure URLs are valid. return array_values( array_filter( $settings, static function ( $url ): bool { return ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) !== false; } ) ); } /** * Get the deepest product category hierarchy for a product. * * Returns the category chain from root to the deepest assigned category, * so the breadcrumb reflects the full taxonomy path. * * @param \WC_Product $product The product. * * @return array Ordered terms from root ancestor to deepest. */ private function get_product_categories( \WC_Product $product ): array { $terms = get_the_terms( $product->get_id(), 'product_cat' ); if ( ! is_array( $terms ) || empty( $terms ) ) { return array(); } // Find the deepest term (most ancestors). $deepest = null; $max_depth = -1; foreach ( $terms as $term ) { $ancestors = get_ancestors( $term->term_id, 'product_cat', 'taxonomy' ); $depth = count( $ancestors ); if ( $depth > $max_depth ) { $max_depth = $depth; $deepest = $term; } } if ( null === $deepest ) { return array(); } // Build the chain from root to deepest. $ancestors = get_ancestors( $deepest->term_id, 'product_cat', 'taxonomy' ); $chain = array(); // Ancestors are returned deepest-first, reverse for root-first. $ancestors = array_reverse( $ancestors ); foreach ( $ancestors as $ancestor_id ) { $ancestor_term = get_term( $ancestor_id, 'product_cat' ); if ( $ancestor_term instanceof \WP_Term ) { $chain[] = $ancestor_term; } } // Add the deepest term itself. $chain[] = $deepest; return $chain; } }