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