query_vars['airewp_file'] ?? ''; if ( empty( $file ) ) { return; } $settings = AI_Ready_WP::get_settings(); switch ( $file ) { case 'agents-json': if ( ! $settings['enable_agents_json'] ) { return; } $this->serve_json_file( '.well-known/agents.json' ); break; case 'agent-card-json': if ( ! $settings['enable_agent_card'] ) { $this->send_404(); return; } $this->serve_json_file( '.well-known/agent-card.json' ); break; case 'agent-json': if ( ! $settings['enable_agent_card'] ) { $this->send_404(); return; } $this->serve_json_file( '.well-known/agent.json' ); break; case 'mcp-json': if ( ! $settings['enable_mcp_json'] ) { $this->send_404(); return; } AIREWP_MCP_JSON::serve(); break; case 'llms-txt': if ( ! $settings['enable_llms_txt'] ) { return; } $this->serve_text_file( 'llms.txt' ); break; case 'llms-full-txt': if ( ! $settings['enable_llms_txt'] ) { return; } $this->serve_text_file( 'llms-full.txt' ); break; case 'indexnow-key': if ( ! $settings['enable_indexnow'] ) { $this->send_404(); return; } $key = $wp->query_vars['airewp_key'] ?? ''; if ( $key === $settings['indexnow_key'] ) { $this->send_text_response( $key ); } break; } } private function serve_json_file( string $path ): void { $file = AIREWP_ROOT_PATH . $path; $fs = AI_Ready_WP::filesystem(); if ( null === $fs ) { $this->send_404(); return; } if ( ! $fs->exists( $file ) ) { self::regenerate_on_demand(); } if ( $fs->exists( $file ) ) { $content = $fs->get_contents( $file ); if ( false !== $content ) { $decoded = json_decode( $content ); if ( null === $decoded ) { $this->send_404(); return; } $this->send_response_headers( 'json' ); echo wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); exit; } } // Falling through here handed the request back to WordPress, which then // canonical-redirected the .json URL and ended up answering with an HTML // page. Say plainly that the file is not there. $this->send_404(); } private function serve_text_file( string $path ): void { $file = AIREWP_ROOT_PATH . $path; $fs = AI_Ready_WP::filesystem(); if ( null === $fs ) { $this->send_404(); return; } if ( ! $fs->exists( $file ) ) { self::regenerate_on_demand(); } if ( $fs->exists( $file ) ) { $content = $fs->get_contents( $file ); if ( false !== $content ) { $this->send_response_headers( 'text' ); // Text content generated by this plugin, served as plain text (not HTML). echo wp_kses( $content, [] ); exit; } } // See the note in serve_json_file(). $this->send_404(); } /** * Rebuild the generated files for a visitor request, at most once a minute. * * These two methods run for anonymous callers, so a site where the files * cannot be written turns every hit on /llms.txt into two WP_Query runs over * a hundred posts each plus two failed writes — and crawlers are pointed at * that URL by robots.txt. The throttle bounds the damage to one attempt a * minute; it is not a lock and does not pretend to be one. */ private static function regenerate_on_demand(): void { if ( get_transient( 'airewp_ondemand_rebuild' ) ) { return; } set_transient( 'airewp_ondemand_rebuild', 1, MINUTE_IN_SECONDS ); AI_Ready_WP::instance()->regenerate_all(); } private function send_404(): void { status_header( 404 ); header( 'Content-Type: application/json' ); echo wp_json_encode( [ 'error' => 'Not found' ] ); exit; } /** * Send common response headers with whitelisted content type. */ private function send_response_headers( string $type ): void { $types = [ 'json' => 'application/json', 'text' => 'text/plain; charset=utf-8', ]; $content_type = $types[ $type ] ?? 'text/plain'; header( 'Content-Type: ' . $content_type ); header( 'Cache-Control: public, max-age=86400, stale-while-revalidate=604800' ); header( 'Access-Control-Allow-Origin: *' ); header( 'Access-Control-Allow-Methods: GET' ); header( 'X-Robots-Tag: noindex' ); } /** * Send a plain text response (for IndexNow key verification). */ private function send_text_response( string $content ): void { $this->send_response_headers( 'text' ); echo esc_html( $content ); exit; } }