' . "\n"; echo ' injection in JSON values echo wp_json_encode( $schemas, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . "\n"; echo '' . "\n"; } /** * Generate schema data. * * @param int $post_id Post ID. * @param string $summary AEO summary. * @return array Schema data (can be single schema or @graph array). */ private static function generate_schema( $post_id, $summary ) { $post = get_post( $post_id ); if ( ! $post ) { return array(); } $schemas = array(); // Generate Article schema (primary) $article_schema = self::generate_article_schema( $post, $summary ); if ( ! empty( $article_schema ) ) { $schemas[] = $article_schema; } // Generate FAQ schema if applicable $faq_schema = self::generate_faq_schema( $post ); if ( ! empty( $faq_schema ) ) { $schemas[] = $faq_schema; } // Generate Service schema if applicable if ( $post->post_type === 'service' ) { $service_schema = self::generate_service_schema( $post, $summary ); if ( ! empty( $service_schema ) ) { $schemas[] = $service_schema; } } // If multiple schemas, wrap in @graph if ( count( $schemas ) > 1 ) { return array( '@context' => 'https://schema.org', '@graph' => $schemas, ); } elseif ( count( $schemas ) === 1 ) { return $schemas[0]; } return array(); } /** * Generate Article schema. * * @param WP_Post $post Post object. * @param string $summary AEO summary. * @return array Article schema. */ private static function generate_article_schema( $post, $summary ) { $schema = array( '@context' => 'https://schema.org', '@type' => 'Article', 'headline' => get_the_title( $post->ID ), ); // Add description (AEO summary) if ( ! empty( $summary ) ) { $schema['description'] = wp_strip_all_tags( $summary ); } // Add author $author_id = $post->post_author; if ( $author_id ) { $author_name = get_the_author_meta( 'display_name', $author_id ); if ( $author_name ) { $schema['author'] = array( '@type' => 'Person', 'name' => $author_name, ); // Add author URL if available $author_url = get_author_posts_url( $author_id ); if ( $author_url ) { $schema['author']['url'] = $author_url; } } } // Add publisher $publisher = self::get_publisher_schema(); if ( ! empty( $publisher ) ) { $schema['publisher'] = $publisher; } // Add dates $schema['datePublished'] = get_the_date( 'c', $post->ID ); $schema['dateModified'] = get_the_modified_date( 'c', $post->ID ); // Add main entity of page $schema['mainEntityOfPage'] = array( '@type' => 'WebPage', '@id' => get_permalink( $post->ID ), ); // Add image if featured image exists $thumbnail_id = get_post_thumbnail_id( $post->ID ); if ( $thumbnail_id ) { $image_url = get_the_post_thumbnail_url( $post->ID, 'full' ); if ( $image_url ) { $schema['image'] = array( '@type' => 'ImageObject', 'url' => $image_url, ); // Add image dimensions if available $image_meta = wp_get_attachment_metadata( $thumbnail_id ); if ( ! empty( $image_meta['width'] ) && ! empty( $image_meta['height'] ) ) { $schema['image']['width'] = $image_meta['width']; $schema['image']['height'] = $image_meta['height']; } } } // Allow filtering return apply_filters( 'ai_answer_toolkit_schema_article', $schema, $post, $summary ); } /** * Generate FAQ schema from post content. * * @param WP_Post $post Post object. * @return array FAQ schema or empty array. */ private static function generate_faq_schema( $post ) { $content = get_post_field( 'post_content', $post->ID ); if ( empty( $content ) ) { return array(); } // Parse Gutenberg blocks $blocks = parse_blocks( $content ); if ( empty( $blocks ) ) { return array(); } // Extract FAQ items $faq_items = self::extract_faq_items( $blocks ); if ( empty( $faq_items ) ) { return array(); } $schema = array( '@type' => 'FAQPage', 'mainEntity' => $faq_items, ); // Allow filtering return apply_filters( 'ai_answer_toolkit_schema_faq', $schema, $post, $faq_items ); } /** * Extract FAQ items from Gutenberg blocks. * * Looks for heading blocks ending with "?" followed by paragraph blocks. * * @param array $blocks Parsed Gutenberg blocks. * @return array FAQ items in schema format. */ private static function extract_faq_items( $blocks ) { $faq_items = array(); for ( $i = 0; $i < count( $blocks ); $i++ ) { $block = $blocks[ $i ]; if ( empty( $block['blockName'] ) ) { continue; } // Check for heading blocks if ( $block['blockName'] === 'core/heading' ) { $question = self::get_block_text_content( $block ); // Check if it's a question (ends with ?) if ( ! empty( $question ) && substr( trim( $question ), -1 ) === '?' ) { // Look for answer in next block(s) $answer = ''; // Check next block for paragraph if ( isset( $blocks[ $i + 1 ] ) && $blocks[ $i + 1 ]['blockName'] === 'core/paragraph' ) { $answer = self::get_block_text_content( $blocks[ $i + 1 ] ); } if ( ! empty( $answer ) ) { $faq_items[] = array( '@type' => 'Question', 'name' => wp_strip_all_tags( $question ), 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => wp_strip_all_tags( $answer ), ), ); } } } // Recursively check inner blocks if ( ! empty( $block['innerBlocks'] ) ) { $inner_faq = self::extract_faq_items( $block['innerBlocks'] ); if ( ! empty( $inner_faq ) ) { $faq_items = array_merge( $faq_items, $inner_faq ); } } } return $faq_items; } /** * Generate Service schema. * * @param WP_Post $post Post object. * @param string $summary AEO summary. * @return array Service schema. */ private static function generate_service_schema( $post, $summary ) { $schema = array( '@type' => 'Service', 'name' => get_the_title( $post->ID ), 'description' => wp_strip_all_tags( $summary ), ); // Add provider (site organization) $provider = self::get_publisher_schema(); if ( ! empty( $provider ) ) { $schema['provider'] = $provider; } // Add service URL $schema['url'] = get_permalink( $post->ID ); // Add area served (can be customized via post meta) $area_served = get_post_meta( $post->ID, '_aitk_service_area', true ); if ( ! empty( $area_served ) ) { $schema['areaServed'] = array( '@type' => 'Place', 'name' => $area_served, ); } // Add service type if available $service_type = get_post_meta( $post->ID, '_aitk_service_type', true ); if ( ! empty( $service_type ) ) { $schema['serviceType'] = $service_type; } // Add image if featured image exists $thumbnail_id = get_post_thumbnail_id( $post->ID ); if ( $thumbnail_id ) { $image_url = get_the_post_thumbnail_url( $post->ID, 'full' ); if ( $image_url ) { $schema['image'] = $image_url; } } // Allow filtering return apply_filters( 'ai_answer_toolkit_schema_service', $schema, $post, $summary ); } /** * Get publisher schema (organization). * * @return array Publisher schema. */ private static function get_publisher_schema() { $schema = array( '@type' => 'Organization', 'name' => get_bloginfo( 'name' ), ); // Add site URL $schema['url'] = home_url( '/' ); // Add logo if available $custom_logo_id = get_theme_mod( 'custom_logo' ); if ( $custom_logo_id ) { $logo_url = wp_get_attachment_image_url( $custom_logo_id, 'full' ); if ( $logo_url ) { $schema['logo'] = array( '@type' => 'ImageObject', 'url' => $logo_url, ); } } return $schema; } /** * Get clean text content from a Gutenberg block. * * @param array $block Gutenberg block. * @return string Block content as plain text. */ private static function get_block_text_content( $block ) { if ( empty( $block['innerHTML'] ) ) { return ''; } $content = $block['innerHTML']; // Remove shortcodes $content = strip_shortcodes( $content ); // Remove HTML tags $content = wp_strip_all_tags( $content ); // Remove extra whitespace $content = preg_replace( '/\s+/', ' ', $content ); // Trim $content = trim( $content ); return $content; } } AI_Answer_Toolkit_Schema::init();