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;
}
}