'GET', 'callback' => array( $this, 'rest_verify' ), 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'fresh' => array( 'description' => 'Force a fresh check, bypassing the 5-minute cache.', 'type' => 'boolean', 'default' => false, ), ), ) ); } public function rest_verify( WP_REST_Request $request ) { $fresh = (bool) $request->get_param( 'fresh' ); if ( ! $fresh ) { $cached = get_transient( self::CACHE_KEY ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } } $results = $this->run_checks(); set_transient( self::CACHE_KEY, $results, self::CACHE_TTL ); return rest_ensure_response( $results ); } /** * Run all verification checks. * * @return array{ * checked_at:int, * checks:array * } */ public function run_checks(): array { $checks = array(); $home_url = home_url( '/' ); // 1. llms.txt $checks[] = $this->check_llms_txt(); // 2. robots.txt with AI bot directives $checks[] = $this->check_robots_txt(); // 3. Schema on a real post $checks[] = $this->check_schema_on_post(); // 4. Coverage — CPTs and builders detected $checks[] = $this->check_coverage(); // 5. FAQ block usage $checks[] = $this->check_faq_usage(); // 6. Author bios $checks[] = $this->check_author_bios(); // 7. Sample post audit score $checks[] = $this->check_sample_audit(); return array( 'checked_at' => time(), 'home_url' => $home_url, 'checks' => $checks, ); } private function check_llms_txt(): array { $enabled = (bool) AEOMATIC_Settings::get( 'llms_txt', 'enabled' ); $url = home_url( '/llms.txt' ); if ( ! $enabled ) { return $this->result( 'llms_txt', __( 'llms.txt endpoint', 'aeomatic' ), 'off', __( 'Disabled in settings. Turn it on to give AI engines a clean site index.', 'aeomatic' ), null, null ); } $response = wp_remote_get( $url, array( 'timeout' => 5, 'redirection' => 2 ) ); if ( is_wp_error( $response ) ) { return $this->result( 'llms_txt', __( 'llms.txt endpoint', 'aeomatic' ), 'fail', /* translators: %s: error message */ sprintf( __( 'Could not reach %s.', 'aeomatic' ), esc_url( $url ) ), $url, $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( 200 !== $code ) { return $this->result( 'llms_txt', __( 'llms.txt endpoint', 'aeomatic' ), 'fail', /* translators: %d: HTTP status code */ sprintf( __( 'Endpoint returned HTTP %d. Visit Settings → Permalinks and click Save to refresh rewrite rules.', 'aeomatic' ), $code ), $url, null ); } $line_count = substr_count( $body, "\n" ); $kb = round( strlen( $body ) / 1024, 1 ); return $this->result( 'llms_txt', __( 'llms.txt endpoint', 'aeomatic' ), 'ok', /* translators: 1: KB size 2: line count */ sprintf( __( 'Live and serving content (%1$s KB, %2$d lines). Click to view.', 'aeomatic' ), $kb, $line_count ), $url, null ); } private function check_robots_txt(): array { $url = home_url( '/robots.txt' ); $response = wp_remote_get( $url, array( 'timeout' => 5, 'redirection' => 2 ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return $this->result( 'robots', __( 'robots.txt with AI directives', 'aeomatic' ), 'fail', __( 'Could not fetch robots.txt.', 'aeomatic' ), $url, null ); } $body = wp_remote_retrieve_body( $response ); if ( false === strpos( $body, 'AEOmatic — AI based Answer Engine Optimization' ) ) { return $this->result( 'robots', __( 'robots.txt with AI directives', 'aeomatic' ), 'warn', __( 'AEOmatic — AI based Answer Engine Optimization directives not detected. Another plugin or a static robots.txt may be overriding ours.', 'aeomatic' ), $url, null ); } // Count how many bots we're managing. $crawlers = (array) AEOMATIC_Settings::get( 'crawlers' ); $bot_count = count( $crawlers ); $blocked_count = count( array_filter( $crawlers, static function ( $v ) { return ! $v; } ) ); return $this->result( 'robots', __( 'robots.txt with AI directives', 'aeomatic' ), 'ok', /* translators: 1: total bots, 2: blocked count */ sprintf( __( 'Managing %1$d AI crawlers (%2$d blocked). Click to view live robots.txt.', 'aeomatic' ), $bot_count, $blocked_count ), $url, null ); } private function check_schema_on_post(): array { $post_types = AEOMATIC_Post_Types::get_enabled_slugs(); if ( empty( $post_types ) ) { $post_types = array( 'post' ); } $recent = get_posts( array( 'post_type' => $post_types, 'post_status' => 'publish', 'numberposts' => 1, 'orderby' => 'date', 'order' => 'DESC', ) ); if ( empty( $recent ) ) { return $this->result( 'schema', __( 'JSON-LD schema on posts', 'aeomatic' ), 'warn', __( 'No published posts to verify against. Publish a post to see schema in action.', 'aeomatic' ), null, null ); } $post = $recent[0]; $post_url = get_permalink( $post ); $response = wp_remote_get( $post_url, array( 'timeout' => 8, 'redirection' => 2, 'user-agent' => 'AEOmatic — AI based Answer Engine Optimization Verifier/' . AEOMATIC_VERSION, ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return $this->result( 'schema', __( 'JSON-LD schema on posts', 'aeomatic' ), 'warn', /* translators: %s: post URL */ sprintf( __( 'Could not fetch %s to verify schema. Plugin output is correct, but a caching layer or login wall may be blocking the check.', 'aeomatic' ), esc_url( $post_url ) ), $post_url, null ); } $body = wp_remote_retrieve_body( $response ); // Find all JSON-LD blocks. preg_match_all( '/]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $body, $matches ); $json_blocks = $matches[1] ?? array(); if ( empty( $json_blocks ) ) { return $this->result( 'schema', __( 'JSON-LD schema on posts', 'aeomatic' ), 'fail', __( 'No JSON-LD found on the latest post. Check that schema types are enabled in Settings.', 'aeomatic' ), $post_url, null ); } $types = array(); foreach ( $json_blocks as $json ) { $decoded = json_decode( $json, true ); if ( is_array( $decoded ) && isset( $decoded['@type'] ) ) { $types[] = is_array( $decoded['@type'] ) ? implode( '+', $decoded['@type'] ) : $decoded['@type']; } } $types = array_values( array_unique( $types ) ); // Build a Google Rich Results Test URL for one-click validation. $rich_test_url = 'https://search.google.com/test/rich-results?url=' . rawurlencode( $post_url ); // translators: 1: number of schema blocks found, 2: comma-separated schema type names. $aeomatic_msg_str = __( 'Found %1$d schema block(s) on your latest post: %2$s. Click "Validate" to test with Google.', 'aeomatic' ); $message = sprintf( $aeomatic_msg_str, count( $json_blocks ), implode( ', ', $types ) ); return $this->result( 'schema', __( 'JSON-LD schema on posts', 'aeomatic' ), 'ok', $message, $rich_test_url, $post_url ); } private function check_coverage(): array { $enabled = AEOMATIC_Post_Types::get_enabled_slugs(); $objects = AEOMATIC_Post_Types::get_public_post_types(); $total = count( $objects ); $on = count( $enabled ); // Sample up to 20 recent items across enabled types and detect builders. $sample = get_posts( array( 'post_type' => $enabled ?: array( 'post' ), 'post_status' => 'publish', 'numberposts' => 20, 'orderby' => 'date', 'order' => 'DESC', ) ); $builders = array(); foreach ( $sample as $post ) { $b = AEOMATIC_Content_Extractor::detect_builder( $post ); $builders[ $b ] = ( $builders[ $b ] ?? 0 ) + 1; } $wc = class_exists( 'WooCommerce' ) ? __( 'WooCommerce: detected, Product schema enabled', 'aeomatic' ) : ''; $acf = function_exists( 'get_field_objects' ) ? __( 'ACF: detected, fields scanned', 'aeomatic' ) : ''; $builder_summary = array(); foreach ( $builders as $slug => $count ) { if ( 'unknown' === $slug ) continue; $builder_summary[] = AEOMATIC_Content_Extractor::builder_label( $slug ) . ' (' . $count . ')'; } $pieces = array(); $pieces[] = sprintf( /* translators: 1: enabled count, 2: total count */ __( '%1$d of %2$d post types active.', 'aeomatic' ), $on, $total ); if ( $builder_summary ) { $pieces[] = __( 'Builders found:', 'aeomatic' ) . ' ' . implode( ', ', $builder_summary ); } if ( $wc ) $pieces[] = $wc; if ( $acf ) $pieces[] = $acf; return $this->result( 'coverage', __( 'Content coverage', 'aeomatic' ), $on > 0 ? 'ok' : 'warn', implode( ' · ', $pieces ), admin_url( 'admin.php?page=' . AEOMATIC_Admin::SLUG_SETTINGS ), null ); } private function check_faq_usage(): array { global $wpdb; $enabled = AEOMATIC_Post_Types::get_enabled_slugs(); if ( empty( $enabled ) ) { $enabled = array( 'post' ); } // Build safe IN clause — one %s placeholder per post type. // The number of placeholders is dynamic so we build the query string first, // then pass the full args array to prepare(). The phpcs ignore is required // because PHPCS cannot statically verify dynamic placeholder counts. $aeomatic_placeholders = implode( ', ', array_fill( 0, count( $enabled ), '%s' ) ); $aeomatic_args = array_merge( array( 'publish' ), $enabled, array( '%