.
*
* Hooked into 'wp_head' at priority 5.
* Skips injection if a known SEO plugin already outputs equivalent schema.
*
* @since 1.0.0
*/
public function inject_json_ld(): void {
if ( ! get_option( 'gaie_semantic_markup', '1' ) ) {
return;
}
$schemas = array();
// WebSite schema — only add if no SEO plugin would duplicate it.
if ( ! $this->seo_plugin_active() && is_front_page() ) {
$schemas[] = $this->build_website_schema();
}
// Organization schema on front page.
if ( ! $this->seo_plugin_active() && is_front_page() ) {
$org = $this->build_organization_schema();
if ( $org ) {
$schemas[] = $org;
}
}
// Article schema for singular posts.
if ( is_singular( 'post' ) && ! $this->seo_plugin_handles_article() ) {
$schemas[] = $this->build_article_schema();
}
// BreadcrumbList on non-front pages.
if ( ! is_front_page() && ! $this->seo_plugin_handles_breadcrumbs() ) {
$breadcrumb = $this->build_breadcrumb_schema();
if ( $breadcrumb ) {
$schemas[] = $breadcrumb;
}
}
// FAQPage schema if the post has FAQ blocks.
if ( is_singular() ) {
$faq = $this->build_faq_schema();
if ( $faq ) {
$schemas[] = $faq;
}
}
// Inject AI-specific semantic markup if a SEO plugin is active
if ( $this->seo_plugin_active() ) {
$schemas[] = $this->build_ai_semantic_schema();
}
foreach ( $schemas as $schema ) {
$json = wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
if ( $json ) {
echo '\n";
}
}
}
/**
* Optionally enrich post content (filter: the_content).
*
* Currently a no-op wrapper; reserved for future semantic attribute injection.
*
* @since 1.0.0
*
* @param string $content The post content.
* @return string Unchanged or enriched content.
*/
public function enrich_content_markup( string $content ): string {
if ( ! get_option( 'gaie_semantic_markup', '1' ) ) {
return $content;
}
// Reserved: future heading-level aria-label enrichment, etc.
return $content;
}
// -------------------------------------------------------------------------
// Schema Builders
// -------------------------------------------------------------------------
/**
* Build the WebSite schema array.
*
* @since 1.0.0
* @return array
*/
private function build_website_schema(): array {
return array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => sanitize_text_field( get_bloginfo( 'name' ) ),
'description' => sanitize_text_field( get_bloginfo( 'description' ) ),
'url' => esc_url_raw( get_home_url() ),
'inLanguage' => sanitize_text_field( get_bloginfo( 'language' ) ),
'potentialAction' => array(
'@type' => 'SearchAction',
'target' => array(
'@type' => 'EntryPoint',
'urlTemplate' => esc_url_raw( get_home_url() ) . '/?s={search_term_string}',
),
'query-input' => 'required name=search_term_string',
),
);
}
/**
* Build the Organization schema array.
*
* @since 1.0.0
* @return array|null
*/
private function build_organization_schema(): ?array {
$name = sanitize_text_field( get_bloginfo( 'name' ) );
if ( empty( $name ) ) {
return null;
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Organization',
'name' => $name,
'url' => esc_url_raw( get_home_url() ),
);
// Add logo if a site icon is set.
$site_icon_id = get_option( 'site_icon' );
if ( $site_icon_id ) {
$logo_src = wp_get_attachment_image_src( (int) $site_icon_id, 'full' );
if ( $logo_src ) {
$schema['logo'] = array(
'@type' => 'ImageObject',
'url' => esc_url_raw( $logo_src[0] ),
);
}
}
return $schema;
}
/**
* Build the Article schema for the current singular post.
*
* @since 1.0.0
* @return array
*/
private function build_article_schema(): array {
global $post;
$author_name = sanitize_text_field( get_the_author_meta( 'display_name', (int) $post->post_author ) );
$published = get_the_date( 'c', $post );
$modified = get_the_modified_date( 'c', $post );
$featured_img = null;
if ( has_post_thumbnail( $post->ID ) ) {
$img_src = wp_get_attachment_image_src( (int) get_post_thumbnail_id( $post->ID ), 'large' );
if ( $img_src ) {
$featured_img = array(
'@type' => 'ImageObject',
'url' => esc_url_raw( $img_src[0] ),
'width' => absint( $img_src[1] ),
'height' => absint( $img_src[2] ),
);
}
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => sanitize_text_field( get_the_title( $post ) ),
'description' => sanitize_text_field( wp_strip_all_tags( get_the_excerpt( $post ) ) ),
'url' => esc_url_raw( get_permalink( $post ) ),
'datePublished' => $published,
'dateModified' => $modified,
'inLanguage' => sanitize_text_field( get_bloginfo( 'language' ) ),
'author' => array(
'@type' => 'Person',
'name' => $author_name,
'url' => esc_url_raw( get_author_posts_url( (int) $post->post_author ) ),
),
'publisher' => array(
'@type' => 'Organization',
'name' => sanitize_text_field( get_bloginfo( 'name' ) ),
'url' => esc_url_raw( get_home_url() ),
),
);
if ( $featured_img ) {
$schema['image'] = $featured_img;
}
return $schema;
}
/**
* Build a BreadcrumbList schema for the current page.
*
* @since 1.0.0
* @return array|null
*/
private function build_breadcrumb_schema(): ?array {
$items = array();
$position = 1;
// Home.
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => sanitize_text_field( __( 'Home', 'adriano-geo-context-crawl-monitor' ) ),
'item' => esc_url_raw( get_home_url() ),
);
// Category archive.
if ( is_category() ) {
$category = get_queried_object();
if ( $category instanceof WP_Term ) {
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => sanitize_text_field( $category->name ),
'item' => esc_url_raw( get_term_link( $category ) ),
);
}
}
// Singular post.
if ( is_singular() ) {
global $post;
$categories = get_the_category( $post->ID );
if ( ! empty( $categories ) ) {
$cat = $categories[0];
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => sanitize_text_field( $cat->name ),
'item' => esc_url_raw( get_category_link( $cat->term_id ) ),
);
}
$items[] = array(
'@type' => 'ListItem',
'position' => $position,
'name' => sanitize_text_field( get_the_title( $post ) ),
'item' => esc_url_raw( get_permalink( $post ) ),
);
}
if ( count( $items ) < 2 ) {
return null;
}
return array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items,
);
}
/**
* Build FAQPage schema by parsing FAQ-style content from the current post.
*
* Looks for headings followed by paragraph blocks (H3 pattern).
*
* @since 1.0.0
* @return array|null
*/
private function build_faq_schema(): ?array {
global $post;
if ( ! $post instanceof WP_Post ) {
return null;
}
$content = $post->post_content;
if ( empty( $content ) ) {
return null;
}
// Parse blocks if Gutenberg is active.
$faq_pairs = array();
if ( function_exists( 'parse_blocks' ) ) {
$blocks = parse_blocks( $content );
foreach ( $blocks as $block ) {
if ( 'core/heading' === $block['blockName'] && isset( $block['attrs']['level'] ) && 3 === (int) $block['attrs']['level'] ) {
$question = wp_strip_all_tags( render_block( $block ) );
$faq_pairs[] = array( 'q' => $question, 'a' => '' );
} elseif ( 'core/paragraph' === $block['blockName'] && ! empty( $faq_pairs ) ) {
$last = count( $faq_pairs ) - 1;
if ( empty( $faq_pairs[ $last ]['a'] ) ) {
$faq_pairs[ $last ]['a'] = wp_strip_all_tags( render_block( $block ) );
}
}
}
}
// Filter out pairs where either question or answer is empty.
$entities = array();
foreach ( $faq_pairs as $pair ) {
$q = sanitize_text_field( trim( $pair['q'] ) );
$a = sanitize_text_field( trim( $pair['a'] ) );
if ( $q && $a ) {
$entities[] = array(
'@type' => 'Question',
'name' => $q,
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $a,
),
);
}
}
if ( empty( $entities ) ) {
return null;
}
return array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => $entities,
);
}
// -------------------------------------------------------------------------
// SEO Plugin Conflict Detection
// -------------------------------------------------------------------------
/**
* Check if a known SEO plugin is active.
*
* @since 1.0.0
* @return bool
*/
private function seo_plugin_active(): bool {
return $this->yoast_active() || $this->rankmath_active() || $this->seopress_active() || $this->allinseopack_active();
}
/**
* Check if Yoast SEO handles article schema.
*
* @since 1.0.0
* @return bool
*/
private function seo_plugin_handles_article(): bool {
return $this->yoast_active() || $this->rankmath_active();
}
/**
* Check if the active SEO plugin outputs breadcrumbs schema.
*
* @since 1.0.0
* @return bool
*/
private function seo_plugin_handles_breadcrumbs(): bool {
return $this->yoast_active() || $this->rankmath_active();
}
/**
* Is Yoast SEO active?
*
* @since 1.0.0
* @return bool
*/
private function yoast_active(): bool {
return defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Frontend', false );
}
/**
* Is RankMath active?
*
* @since 1.0.0
* @return bool
*/
private function rankmath_active(): bool {
return defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath', false );
}
/**
* Is SEOPress active?
*
* @since 1.0.0
* @return bool
*/
private function seopress_active(): bool {
return defined( 'SEOPRESS_VERSION' );
}
/**
* Is All in One SEO Pack active?
*
* @since 1.0.0
* @return bool
*/
private function allinseopack_active(): bool {
return defined( 'AIOSEO_VERSION' ) || class_exists( 'All_in_One_SEO_Pack', false );
}
/**
* Build AI-specific semantic metadata schema block.
*
* Extends existing SEO metadata with concept categorization and targets for LLMs.
*
* @since 1.1.0
* @return array
*/
private function build_ai_semantic_schema(): array {
global $post;
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'WebPage',
'@id' => esc_url_raw( get_permalink() ) . '#ai-markup',
'url' => esc_url_raw( get_permalink() ),
'additionalType' => 'https://schema.org/SpecialtyPage',
);
if ( is_singular() && isset( $post ) ) {
$keywords = array();
// Tags
$tags = wp_get_post_terms( $post->ID, 'post_tag', array( 'fields' => 'names' ) );
if ( ! is_wp_error( $tags ) && is_array( $tags ) ) {
$keywords = array_merge( $keywords, $tags );
}
// Categories
$cats = wp_get_post_terms( $post->ID, 'category', array( 'fields' => 'names' ) );
if ( ! is_wp_error( $cats ) && is_array( $cats ) ) {
$keywords = array_merge( $keywords, $cats );
}
if ( ! empty( $keywords ) ) {
$about_list = array();
foreach ( array_slice( $keywords, 0, 5 ) as $kw ) {
$about_list[] = array(
'@type' => 'Thing',
'name' => sanitize_text_field( $kw ),
);
}
$schema['about'] = $about_list;
}
$schema['audience'] = array(
'@type' => 'Audience',
'audienceType' => 'AI Agents and Large Language Models',
);
}
return $schema;
}
}