article_schema = $article_schema; $this->webpage_schema = $webpage_schema; } /** * Inject content JSON-LD into wp_head. * * @return void */ public function inject(): void { $post = $this->target_post(); if ( null === $post ) { return; } $schemas = $this->build_schemas( $post ); if ( empty( $schemas ) ) { return; } 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. ); } } /** * Resolve the current singular post this engine should handle. * * Returns null on product pages (handled by SchemaEngine), non-singular * views, or when schema output is disabled. * * @return \WP_Post|null */ private function target_post(): ?\WP_Post { if ( ! get_option( 'aicommerce_schema_enabled', true ) ) { return null; } if ( ! is_singular() ) { return null; } // Products are handled by the ecommerce SchemaEngine. if ( function_exists( 'is_product' ) && is_product() ) { return null; } $post = get_post(); if ( ! $post instanceof \WP_Post ) { return null; } // Never emit content schema for the product post type. if ( 'product' === $post->post_type ) { return null; } return $post; } /** * Build the schema objects for a post. * * @param \WP_Post $post The post. * * @return array> */ public function build_schemas( \WP_Post $post ): array { $schemas = array(); $primary = $this->build_primary( $post ); if ( ! empty( $primary ) ) { $schemas[] = $primary; } $breadcrumb = $this->build_breadcrumb( $post ); if ( ! empty( $breadcrumb ) ) { $schemas[] = $breadcrumb; } /** * Filters the content schema objects before output. * * @param array> $schemas The schema objects. * @param \WP_Post $post The post. */ return (array) apply_filters( 'aicommerce_content_schemas', array_values( array_filter( $schemas ) ), $post ); } /** * Build the primary schema (Article for posts, WebPage for pages/others). * * @param \WP_Post $post The post. * * @return array */ private function build_primary( \WP_Post $post ): array { /** * Filters which post types are treated as articles (BlogPosting). * * @param array $article_types Post types rendered as articles. */ $article_types = (array) apply_filters( 'aicommerce_article_post_types', array( 'post' ) ); if ( in_array( $post->post_type, $article_types, true ) ) { return $this->article_schema->generate( $post ); } return $this->webpage_schema->generate( $post ); } /** * Build a BreadcrumbList for the current post or page. * * @param \WP_Post $post The post. * * @return array */ private function build_breadcrumb( \WP_Post $post ): array { $items = array(); $pos = 1; $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => get_bloginfo( 'name' ), 'item' => home_url( '/' ), ); if ( 'page' === $post->post_type ) { // Ancestors for hierarchical pages. $ancestors = array_reverse( get_post_ancestors( $post ) ); foreach ( $ancestors as $ancestor_id ) { $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => wp_strip_all_tags( get_the_title( $ancestor_id ) ), 'item' => get_permalink( $ancestor_id ), ); } } else { // Primary category for posts. $term = $this->primary_term( $post ); if ( $term instanceof \WP_Term ) { $link = get_term_link( $term ); if ( ! is_wp_error( $link ) ) { $items[] = array( '@type' => 'ListItem', 'position' => $pos++, 'name' => $term->name, 'item' => $link, ); } } } $items[] = array( '@type' => 'ListItem', 'position' => $pos, 'name' => wp_strip_all_tags( get_the_title( $post ) ), 'item' => get_permalink( $post ), ); // A single Home + self breadcrumb adds no value. if ( count( $items ) < 3 ) { return array(); } return array( '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $items, ); } /** * Get the primary category term for a post. * * @param \WP_Post $post The post. * * @return \WP_Term|null */ private function primary_term( \WP_Post $post ): ?\WP_Term { $terms = get_the_terms( $post, 'category' ); if ( ! is_array( $terms ) || empty( $terms ) ) { return null; } return $terms[0] instanceof \WP_Term ? $terms[0] : null; } }