product_schema = $product_schema; $this->entity_chain = $entity_chain; $this->conflict_detector = $conflict_detector; $this->faq_schema = $faq_schema; $this->speakable_schema = $speakable_schema; } /** * Inject JSON-LD schema into wp_head. * * Hooked at priority 5 so it runs before most theme/plugin output. * Only fires on WooCommerce product pages. * * @return void */ public function inject(): void { if ( ! $this->should_inject() ) { return; } $product = wc_get_product( get_the_ID() ); if ( ! $product instanceof \WC_Product ) { return; } $schemas = $this->build_schemas( $product ); if ( empty( $schemas ) ) { return; } $this->output_json_ld( $schemas ); } /** * Determine if schema should be injected on the current page. * * @return bool */ private function should_inject(): bool { // Only on single product pages (requires WooCommerce). if ( ! function_exists( 'is_product' ) || ! is_product() ) { return false; } // Check if schema injection is enabled. $enabled = get_option( 'aicommerce_schema_enabled', true ); if ( ! $enabled ) { return false; } return true; } /** * Build all schema objects for a product. * * @param \WC_Product $product The WooCommerce product. * * @return array> Array of schema.org objects. */ public function build_schemas( \WC_Product $product ): array { // Check schema cache. $product_id = $product->get_id(); $hash = $this->compute_cache_hash( $product ); $cached = get_post_meta( $product_id, '_aicommerce_schema_cache', true ); if ( is_array( $cached ) && ! empty( $cached ) ) { $stored_hash = (string) get_post_meta( $product_id, '_aicommerce_schema_hash', true ); if ( $stored_hash === $hash ) { /** This filter is documented below. */ return apply_filters( 'aicommerce_schemas', $cached, $product ); } } $schemas = array(); // Product + Offer schema (always included). $product_data = $this->product_schema->generate( $product ); if ( ! empty( $product_data ) ) { // Merge Speakable into Product schema if available. if ( null !== $this->speakable_schema ) { $speakable = $this->speakable_schema->generate( $product ); if ( ! empty( $speakable ) ) { $product_data['speakable'] = $speakable; } } $schemas[] = $product_data; } // BreadcrumbList schema. $breadcrumb = $this->entity_chain->build_breadcrumb( $product ); if ( ! empty( $breadcrumb ) ) { $schemas[] = $breadcrumb; } // FAQPage schema. if ( null !== $this->faq_schema ) { $faq = $this->faq_schema->generate( $product ); if ( ! empty( $faq ) ) { $schemas[] = $faq; } } // Store in cache. update_post_meta( $product_id, '_aicommerce_schema_cache', $schemas ); update_post_meta( $product_id, '_aicommerce_schema_hash', $hash ); /** * Filters the complete array of schema objects before output. * * @param array> $schemas Array of schema.org objects. * @param \WC_Product $product The WooCommerce product. */ $schemas = apply_filters( 'aicommerce_schemas', $schemas, $product ); return $schemas; } /** * Output JSON-LD script tags. * * Each schema object gets its own script tag for maximum parser compatibility. * * @param array> $schemas Schema objects to output. * * @return void */ private function output_json_ld( array $schemas ): void { foreach ( $schemas as $schema ) { $json = wp_json_encode( $schema, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE ); if ( false === $json ) { continue; } printf( '' . "\n", $json // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe JSON-LD: JSON_HEX_TAG|JSON_HEX_AMP escape <, >, & so it cannot break out of the script element. ); } } /** * Get the product schema generator. * * @return ProductSchema */ public function get_product_schema(): ProductSchema { return $this->product_schema; } /** * Get the entity chain builder. * * @return EntityChain */ public function get_entity_chain(): EntityChain { return $this->entity_chain; } /** * Get the conflict detector. * * @return ConflictDetector */ public function get_conflict_detector(): ConflictDetector { return $this->conflict_detector; } /** * Compute a hash for cache invalidation. * * Based on the product modified date and plugin version. * * @param \WC_Product $product The product. * * @return string MD5 hash. */ private function compute_cache_hash( \WC_Product $product ): string { $modified = $product->get_date_modified(); $date_str = $modified ? $modified->date( 'Y-m-d H:i:s' ) : ''; return md5( $date_str . '|' . AICOMMERCE_VERSION ); } /** * Clear the schema cache for a product. * * @param int $product_id The product ID. * * @return void */ public function clear_schema_cache( int $product_id ): void { delete_post_meta( $product_id, '_aicommerce_schema_cache' ); delete_post_meta( $product_id, '_aicommerce_schema_hash' ); } }