%s', esc_url( admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ), esc_html__( 'Settings', 'ai-ready-wp' ) ); array_unshift( $links, $settings_link ); return $links; } public function enqueue_assets( string $hook ): void { if ( 'settings_page_' . self::PAGE_SLUG !== $hook ) { return; } wp_enqueue_style( 'airewp-admin', AIREWP_PLUGIN_URL . 'admin/css/admin.css', [], AIREWP_VERSION ); wp_enqueue_media(); // For logo upload } public function register_settings(): void { register_setting( self::PAGE_SLUG, AIREWP_OPTION_KEY, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_settings' ], ] ); // Business Information add_settings_section( self::SECTION_BUSINESS, __( 'Business Information', 'ai-ready-wp' ), fn() => printf( '

%s

', esc_html__( 'Basic information about your business. Used for agents.json, JSON-LD schemas, and AI agent discovery.', 'ai-ready-wp' ) ), self::PAGE_SLUG ); $this->add_field( self::SECTION_BUSINESS, 'company_name', __( 'Company Name', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_desc', __( 'Description', 'ai-ready-wp' ), 'textarea' ); $this->add_field( self::SECTION_BUSINESS, 'company_email', __( 'Contact Email', 'ai-ready-wp' ), 'email' ); $this->add_field( self::SECTION_BUSINESS, 'company_phone', __( 'Phone', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_address', __( 'Street Address', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_city', __( 'City', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_zip', __( 'ZIP / Postal Code', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_country', __( 'Country', 'ai-ready-wp' ) ); $this->add_field( self::SECTION_BUSINESS, 'company_logo', __( 'Logo URL', 'ai-ready-wp' ), 'url' ); $this->add_field( self::SECTION_BUSINESS, 'industry', __( 'Industry', 'ai-ready-wp' ), 'select', [ 'options' => [ 'general' => __( 'General Business', 'ai-ready-wp' ), 'restaurant' => __( 'Restaurant / Gastronomy', 'ai-ready-wp' ), 'real-estate' => __( 'Real Estate', 'ai-ready-wp' ), 'ecommerce' => __( 'E-Commerce / Shop', 'ai-ready-wp' ), 'saas' => __( 'SaaS / Software', 'ai-ready-wp' ), 'agency' => __( 'Agency / Consulting', 'ai-ready-wp' ), 'medical' => __( 'Medical / Health', 'ai-ready-wp' ), 'legal' => __( 'Legal / Law Firm', 'ai-ready-wp' ), 'finance' => __( 'Finance / Insurance', 'ai-ready-wp' ), 'education' => __( 'Education / Training', 'ai-ready-wp' ), 'construction' => __( 'Construction / Trades', 'ai-ready-wp' ), 'fitness' => __( 'Fitness / Sports', 'ai-ready-wp' ), 'beauty' => __( 'Beauty / Wellness', 'ai-ready-wp' ), 'automotive' => __( 'Automotive', 'ai-ready-wp' ), 'hotel' => __( 'Hotel / Hospitality', 'ai-ready-wp' ), 'nonprofit' => __( 'Nonprofit / Association', 'ai-ready-wp' ), ], ] ); $this->add_field( self::SECTION_BUSINESS, 'services', __( 'Services (comma-separated)', 'ai-ready-wp' ), 'textarea', [ 'description' => __( 'e.g.: Web Design, SEO, Consulting, Photography', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_BUSINESS, 'schema_type', __( 'Business Type', 'ai-ready-wp' ), 'select', [ 'options' => [ 'Organization' => __( 'Organization (company, brand)', 'ai-ready-wp' ), 'LocalBusiness' => __( 'Local Business (physical location)', 'ai-ready-wp' ), ], ] ); // Social Media add_settings_section( self::SECTION_SOCIAL, __( 'Social Media', 'ai-ready-wp' ), fn() => printf( '

%s

', esc_html__( 'Social profiles for JSON-LD sameAs and agent discovery.', 'ai-ready-wp' ) ), self::PAGE_SLUG ); $this->add_field( self::SECTION_SOCIAL, 'social_x', __( 'X (Twitter) URL', 'ai-ready-wp' ), 'url' ); $this->add_field( self::SECTION_SOCIAL, 'social_linkedin', __( 'LinkedIn URL', 'ai-ready-wp' ), 'url' ); $this->add_field( self::SECTION_SOCIAL, 'social_instagram', __( 'Instagram URL', 'ai-ready-wp' ), 'url' ); $this->add_field( self::SECTION_SOCIAL, 'social_facebook', __( 'Facebook URL', 'ai-ready-wp' ), 'url' ); $this->add_field( self::SECTION_SOCIAL, 'social_github', __( 'GitHub URL', 'ai-ready-wp' ), 'url' ); // Feature Toggles add_settings_section( self::SECTION_FEATURES, __( 'Features', 'ai-ready-wp' ), fn() => printf( '

%s

', esc_html__( 'Enable or disable individual AI-Ready features.', 'ai-ready-wp' ) ), self::PAGE_SLUG ); $this->add_field( self::SECTION_FEATURES, 'enable_agents_json', __( 'agents.json', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'AI Agent Discovery — /.well-known/agents.json', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_agent_card', __( 'agent-card.json', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'A2A Protocol — /.well-known/agent-card.json', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_llms_txt', __( 'llms.txt', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'LLM-readable content index — /llms.txt', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_json_ld', __( 'JSON-LD Schemas', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Enhanced structured data in page head', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_indexnow', __( 'IndexNow', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Instant indexing notification to Bing/Yandex on content changes', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_rest_api', __( 'REST API', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'API endpoints for AI agents (/wp-json/airewp/v1/)', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_robots_ai', __( 'AI Bot Management', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Manage AI training and retrieval bots in robots.txt', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_crawler_analytics', __( 'AI Crawler Analytics', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Track which AI bots visit your site and how often', 'ai-ready-wp' ), ] ); $this->add_field( self::SECTION_FEATURES, 'enable_mcp_json', __( 'mcp.json', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'MCP Server Discovery — /.well-known/mcp.json', 'ai-ready-wp' ), ] ); // AI Bot Settings (shown if robots.txt enabled) $settings = AI_Ready_WP::get_settings(); if ( ! empty( $settings['enable_robots_ai'] ) ) { add_settings_section( 'airewp_robots', __( 'AI Bot Policy', 'ai-ready-wp' ), [ $this, 'render_robots_section_intro' ], self::PAGE_SLUG ); $this->add_field( 'airewp_robots', 'robots_block_training', __( 'Block Training Bots', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Answer GPTBot, ClaudeBot, Google-Extended, Meta-ExternalAgent and the other training crawlers with Disallow. Your content then stays out of future models, and out of the answers those models give.', 'ai-ready-wp' ), ] ); $this->add_field( 'airewp_robots', 'robots_block_retrieval', __( 'Block Retrieval Bots', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Answer OAI-SearchBot, Claude-SearchBot, PerplexityBot and the other retrieval crawlers with Disallow. Your site then no longer appears in AI answers. Not recommended.', 'ai-ready-wp' ), ] ); } // Crawler Analytics Stats (shown if enabled) if ( ! empty( $settings['enable_crawler_analytics'] ) ) { add_settings_section( 'airewp_crawler_stats', __( 'AI Crawler Statistics', 'ai-ready-wp' ), [ $this, 'render_crawler_section' ], self::PAGE_SLUG ); } // ─── v4.0 SEO Module ───────────────────────────────── add_settings_section( 'airewp_seo', __( 'SEO', 'ai-ready-wp' ), [ $this, 'render_seo_section_intro' ], self::PAGE_SLUG ); $this->add_field( 'airewp_seo', 'enable_seo', __( 'Enable SEO Module', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Meta title/description, OpenGraph tags, Twitter Cards per page', 'ai-ready-wp' ), ] ); // Only offered when there is something to override. if ( '' !== AIREWP_SEO::competing_plugin() ) { $this->add_field( 'airewp_seo', 'seo_force_with_plugin', __( 'Output anyway', 'ai-ready-wp' ), 'checkbox', [ 'description' => __( 'Print this module\'s meta tags, social tags and sitemap even though another SEO plugin is active. Expect duplicate tags.', 'ai-ready-wp' ), ] ); } // Advanced add_settings_section( self::SECTION_ADVANCED, __( 'Advanced', 'ai-ready-wp' ), fn() => printf( '

%s

', esc_html__( 'Advanced settings for developers and power users.', 'ai-ready-wp' ) ), self::PAGE_SLUG ); $this->add_field( self::SECTION_ADVANCED, 'indexnow_key', __( 'IndexNow Key', 'ai-ready-wp' ), 'text', [ 'description' => __( 'Auto-generated on activation. Only change if you have a custom key.', 'ai-ready-wp' ), 'class' => 'large-text code', ] ); $this->add_field( self::SECTION_ADVANCED, 'mcp_servers', __( 'MCP Servers (JSON)', 'ai-ready-wp' ), 'textarea', [ 'description' => __( 'JSON array of MCP servers. Example: [{"name":"my-server","description":"My MCP Server","transport":"streamable-http","url":"https://example.com/mcp"}]', 'ai-ready-wp' ), ] ); // Connect your AI (v4.5): the five steps to the hosted MCP server. add_settings_section( self::SECTION_CONNECT, __( 'Connect your AI (MCP)', 'ai-ready-wp' ), [ $this, 'render_connect_section' ], self::PAGE_SLUG ); } private function add_field( string $section, string $id, string $label, string $type = 'text', array $args = [] ): void { add_settings_field( $id, $label, [ $this, 'render_field' ], self::PAGE_SLUG, $section, array_merge( $args, [ 'id' => $id, 'type' => $type, ] ) ); } public function render_field( array $args ): void { $settings = AI_Ready_WP::get_settings(); $id = $args['id']; $type = $args['type'] ?? 'text'; $value = $settings[ $id ] ?? ''; $name = AIREWP_OPTION_KEY . '[' . $id . ']'; $class = $args['class'] ?? ''; $desc = $args['description'] ?? ''; switch ( $type ) { case 'textarea': printf( '', esc_attr( $name ), esc_attr( $id ), esc_textarea( $value ) ); break; case 'select': printf( ''; break; case 'checkbox': printf( '', esc_attr( $name ), esc_attr( $id ), checked( $value, true, false ), esc_html( $desc ) ); return; // Don't print description again case 'email': case 'url': case 'text': default: $field_class = $class ?: 'regular-text'; printf( '', esc_attr( $type ), esc_attr( $name ), esc_attr( $id ), esc_attr( $value ), esc_attr( $field_class ) ); break; } if ( $desc && 'checkbox' !== $type ) { printf( '

%s

', esc_html( $desc ) ); } } /** * Render AI Crawler Analytics section. */ public function render_crawler_section(): void { $stats = AIREWP_Crawler_Analytics::get_stats(); $total = AIREWP_Crawler_Analytics::get_total_visits(); $log = AIREWP_Crawler_Analytics::get_log( 10 ); if ( empty( $stats ) ) { printf( '

%s

', esc_html__( 'No AI crawler visits recorded yet. Data appears after bots visit your site.', 'ai-ready-wp' ) ); return; } echo '
'; echo '
'; // Total visits card printf( '
%d%s
', absint( $total ), esc_html__( 'Total AI Visits', 'ai-ready-wp' ) ); // Unique bots card printf( '
%d%s
', count( $stats ), esc_html__( 'Unique Bots', 'ai-ready-wp' ) ); echo '
'; // Per-bot breakdown echo ''; echo ''; echo ''; echo ''; // Sort by visits descending uasort( $stats, fn( $a, $b ) => ( $b['visits'] ?? 0 ) - ( $a['visits'] ?? 0 ) ); foreach ( $stats as $bot => $data ) { printf( '', esc_html( $bot ), absint( $data['visits'] ?? 0 ), esc_html( $data['last_seen'] ?? '—' ) ); } echo '
' . esc_html__( 'Bot', 'ai-ready-wp' ) . '' . esc_html__( 'Visits', 'ai-ready-wp' ) . '' . esc_html__( 'Last Seen', 'ai-ready-wp' ) . '
%s%d%s
'; // Recent log if ( ! empty( $log ) ) { echo '

' . esc_html__( 'Recent Visits', 'ai-ready-wp' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $log as $entry ) { printf( '', esc_html( $entry['bot'] ), esc_html( $entry['url'] ), esc_html( $entry['time'] ) ); } echo '
' . esc_html__( 'Bot', 'ai-ready-wp' ) . '' . esc_html__( 'URL', 'ai-ready-wp' ) . '' . esc_html__( 'Time', 'ai-ready-wp' ) . '
%s%s%s
'; } echo '
'; } public function sanitize_settings( $input ): array { if ( ! is_array( $input ) ) { return []; } $old = AI_Ready_WP::get_settings(); $sanitized = []; // Text fields $text_fields = [ 'company_name', 'company_desc', 'company_phone', 'company_address', 'company_city', 'company_zip', 'company_country', 'services', 'indexnow_key', 'custom_tools', 'custom_skills', ]; foreach ( $text_fields as $field ) { $sanitized[ $field ] = sanitize_text_field( $input[ $field ] ?? $old[ $field ] ?? '' ); } // JSON fields — validate structure, keep raw JSON. // NOTE: no wp_unslash() here — options.php already unslashes the // input before the sanitize callback runs; a second unslash would // strip legitimate escapes inside JSON strings (e.g. \" sequences) // and silently blank out valid input. $mcp_raw = $input['mcp_servers'] ?? $old['mcp_servers'] ?? ''; if ( ! empty( $mcp_raw ) ) { $mcp_parsed = json_decode( $mcp_raw, true ); $sanitized['mcp_servers'] = is_array( $mcp_parsed ) ? wp_json_encode( $mcp_parsed ) : ''; } else { $sanitized['mcp_servers'] = ''; } // Email $sanitized['company_email'] = sanitize_email( $input['company_email'] ?? $old['company_email'] ?? '' ); // URLs $url_fields = [ 'company_url', 'company_logo', 'social_x', 'social_linkedin', 'social_instagram', 'social_facebook', 'social_github', ]; foreach ( $url_fields as $field ) { $sanitized[ $field ] = esc_url_raw( $input[ $field ] ?? $old[ $field ] ?? '' ); } // Select fields $sanitized['industry'] = sanitize_text_field( $input['industry'] ?? $old['industry'] ?? 'general' ); $sanitized['schema_type'] = in_array( $input['schema_type'] ?? '', [ 'Organization', 'LocalBusiness' ], true ) ? $input['schema_type'] : 'Organization'; // Checkboxes (unchecked = not in POST) $checkboxes = [ 'enable_agents_json', 'enable_agent_card', 'enable_llms_txt', 'enable_json_ld', 'enable_indexnow', 'enable_rest_api', 'enable_robots_ai', 'enable_crawler_analytics', 'enable_mcp_json', 'robots_block_training', 'robots_block_retrieval', // v4.0 'enable_seo', // v4.4.3 'seo_force_with_plugin', ]; foreach ( $checkboxes as $cb ) { $sanitized[ $cb ] = ! empty( $input[ $cb ] ); } // Languages $sanitized['languages'] = $old['languages'] ?? [ get_locale() ]; // NOTE: regeneration is triggered from the update_option hooks below, // NOT here — this callback runs BEFORE the option is written, so a // do_action here would regenerate the files from the OLD values. return $sanitized; } /** * Fire the regeneration hook once the option value is actually persisted. * Covers both a changed value (update_option_*) and the first save * (add_option_*). */ public static function trigger_regeneration(): void { do_action( 'airewp_settings_updated' ); } public function render_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $this->maybe_handle_recheck(); $settings = AI_Ready_WP::get_settings(); // This is the screen where a fresh measurement is worth its latency. $score = AIREWP_Dashboard::calculate_score( $settings, true ); ?>

broken_endpoints( $settings ); ?> render_write_warning( ! empty( $broken ) ); ?> render_reachability_warning( $broken ); ?>
/10

render_file_status( 'agents.json', '.well-known/agents.json', $settings['enable_agents_json'] ); ?> render_file_status( 'agent-card.json', '.well-known/agent-card.json', $settings['enable_agent_card'] ); ?> render_file_status( 'agent.json', '.well-known/agent.json', $settings['enable_agent_card'] ); ?> render_mcp_status( $settings ); ?> render_file_status( 'llms.txt', 'llms.txt', $settings['enable_llms_txt'] ); ?> render_file_status( 'llms-full.txt', 'llms-full.txt', $settings['enable_llms_txt'] ); ?> render_file_status( 'IndexNow Key', $settings['indexnow_key'] . '.txt', true ); ?>

render_seo_module_status( $settings ); ?> render_module_status( __( 'AI Discovery', 'ai-ready-wp' ), $settings['enable_agents_json'], __( 'agents.json, llms.txt, agent-card.json', 'ai-ready-wp' ) ); ?>

.

%s

', esc_html( sprintf( /* translators: %d: number of AI crawlers listed in robots.txt */ __( 'robots.txt names all %d known AI crawlers and allows each of them, with a Content-Signal line that says what they may do with your content. Tick a box below to turn a group away instead.', 'ai-ready-wp' ), $count ) ) ); } /** * The five steps from this site to the site owner's own AI. * * The hosted MCP server at wp.studiomeyer.io reads and, with Pro, * configures this plugin through the admin REST API, signed in with an * application password. Nothing is typed into a chat; the credentials go * into a form on that server and are stored there encrypted. */ public function render_connect_section(): void { $allowed = [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ], 'code' => [], ]; $steps = [ sprintf( /* translators: 1: opening link tag to wp.studiomeyer.io, 2: closing link tag */ __( 'Open %1$shttps://wp.studiomeyer.io%2$s in your browser and sign in with Google, Discord or your email.', 'ai-ready-wp' ), '', '' ), sprintf( /* translators: 1: opening link tag to the profile page, 2: closing link tag */ __( 'In WordPress, go to %1$sUsers > Profile > Application Passwords%2$s, enter the name \'AI-Ready WP\' and click \'Add New Application Password\'. Copy the password once, WordPress will not show it again.', 'ai-ready-wp' ), '', '' ), sprintf( /* translators: 1: the MCP endpoint address, 2: the Claude Code command */ __( 'Add the connector in your AI client: the address is %1$s. Claude Desktop: Settings > Connectors > Add custom connector. Claude Code: %2$s. ChatGPT: Settings > Connectors > Create (developer mode).', 'ai-ready-wp' ), 'https://wp.studiomeyer.io/mcp', 'claude mcp add --transport http ai-ready-wp https://wp.studiomeyer.io/mcp' ), __( 'Ask your AI to connect this site. It will hand you a secure link where you paste the site address, your WordPress username and the application password. Nothing is typed into the chat.', 'ai-ready-wp' ), __( 'From then on your AI reads the readiness score, the pages and robots.txt. With AI-Ready WP Pro it may also write: settings, titles and descriptions, rebuilds.', 'ai-ready-wp' ), ]; echo '
    '; foreach ( $steps as $step ) { printf( '
  1. %s
  2. ', wp_kses( $step, $allowed ) ); } echo '
'; printf( '

%s

', esc_html__( 'Credentials are encrypted on the StudioMeyer server and are only used to talk to this site\'s REST API.', 'ai-ready-wp' ) ); // WordPress only offers application passwords over HTTPS. On an // http:// site the profile section from step 2 does not exist, and // a step that points at a section that is not there is the kind of // instruction that costs an afternoon. if ( ! wp_is_application_passwords_available() ) { printf( '

%s

', esc_html__( 'Application passwords need HTTPS. This site is served over HTTP, so WordPress hides that section until the site runs on HTTPS.', 'ai-ready-wp' ) ); } } /** * Intro of the SEO section: names the other SEO plugin when there is one. */ public function render_seo_section_intro(): void { $other = AIREWP_SEO::competing_plugin(); if ( '' === $other ) { printf( '

%s

', esc_html__( 'Search engine optimization. Meta tags, OpenGraph, XML Sitemap.', 'ai-ready-wp' ) ); return; } printf( '

%s

', esc_html( sprintf( /* translators: %s: name of the other SEO plugin */ __( '%s is active. AI-Ready WP leaves titles, descriptions, social tags, the sitemap and the organisation schema to it, so your pages do not carry two of each. agents.json, llms.txt, robots.txt and the FAQ schema are not affected.', 'ai-ready-wp' ), $other ) ) ); } /** * The SEO row of the module table, with the standing-down state. */ private function render_seo_module_status( array $settings ): void { $other = AIREWP_SEO::competing_plugin(); if ( ! empty( $settings['enable_seo'] ) && '' !== $other && AIREWP_SEO::yields() ) { printf( '%s%s%s', esc_html__( 'SEO', 'ai-ready-wp' ), esc_html__( 'Standing down', 'ai-ready-wp' ), esc_html( sprintf( /* translators: %s: name of the other SEO plugin */ __( '%s handles meta tags, social tags, the sitemap and the organisation schema on this site.', 'ai-ready-wp' ), $other ) ) ); return; } $this->render_module_status( __( 'SEO', 'ai-ready-wp' ), (bool) $settings['enable_seo'], __( 'Meta tags, OpenGraph, Twitter Cards', 'ai-ready-wp' ) ); } /** * The mcp.json row. Switched on with nothing to publish is its own state. */ private function render_mcp_status( array $settings ): void { if ( ! empty( $settings['enable_mcp_json'] ) && ! AIREWP_MCP_JSON::is_active( $settings ) ) { printf( '%s%s%s%s', 'mcp.json', esc_html__( 'No servers listed', 'ai-ready-wp' ), esc_html__( 'the URL answers 404 until you add a server under Advanced', 'ai-ready-wp' ), '.well-known/mcp.json' ); return; } $this->render_file_status( 'mcp.json', '.well-known/mcp.json', (bool) $settings['enable_mcp_json'] ); } /** * Tell the site owner once that 4.4.3 stopped blocking AI crawlers. */ public function robots_policy_notice(): void { if ( ! get_option( AI_Ready_WP::ROBOTS_NOTICE_OPTION ) || ! current_user_can( 'manage_options' ) ) { return; } $settings_url = admin_url( 'options-general.php?page=' . self::PAGE_SLUG ); $dismiss_url = wp_nonce_url( add_query_arg( 'airewp_robots_notice_dismiss', '1' ), 'airewp_robots_notice_dismiss' ); echo '

'; echo '' . esc_html__( 'AI-Ready WP now lets AI crawlers in.', 'ai-ready-wp' ) . ' '; echo esc_html__( 'Your robots.txt used to turn away GPTBot, ClaudeBot, Google-Extended and ten other AI crawlers, a default this plugin set without asking. Since that stands in the way of the AI visibility the plugin is for, the block is now off. If you do want to keep your content out of model training, switch it back on under AI Bot Policy.', 'ai-ready-wp' ); echo ' ' . esc_html__( 'Open the settings', 'ai-ready-wp' ) . ''; echo ' · ' . esc_html__( 'Understood', 'ai-ready-wp' ) . ''; echo '

'; } public function handle_robots_notice_dismiss(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified on the next line. if ( empty( $_GET['airewp_robots_notice_dismiss'] ) ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } check_admin_referer( 'airewp_robots_notice_dismiss' ); delete_option( AI_Ready_WP::ROBOTS_NOTICE_OPTION ); wp_safe_redirect( remove_query_arg( [ 'airewp_robots_notice_dismiss', '_wpnonce' ] ) ); exit; } private function render_module_status( string $label, bool $enabled, string $details ): void { $allowed = [ 'span' => [ 'class' => [] ] ]; $status = $enabled ? '' . esc_html__( 'Active', 'ai-ready-wp' ) . '' : '' . esc_html__( 'Disabled', 'ai-ready-wp' ) . ''; printf( '%s%s%s', esc_html( $label ), wp_kses( $status, $allowed ), esc_html( $details ) ); } /** * One row of the discovery table. * * The status column answers "does the URL work", because that is the whole * point of the document. Whether a file was written is shown next to it as * context: it is the difference between the web server answering on its own * and WordPress building the answer, and both are fine. * * The IndexNow key file has no reachability check and still reports file * existence, which for that one really is the deciding fact. */ private function render_file_status( string $label, string $path, bool $enabled ): void { $fs = AI_Ready_WP::filesystem(); $file_exists = null !== $fs && $fs->exists( AIREWP_ROOT_PATH . $path ); $url = home_url( '/' . $path ); $checked = array_key_exists( $path, AIREWP_Endpoint_Check::paths() ); $allowed_html = [ 'span' => [ 'class' => [] ], 'a' => [ 'href' => [], 'target' => [] ], 'code' => [], ]; if ( ! $enabled ) { $status = '' . esc_html__( 'Disabled', 'ai-ready-wp' ) . ''; } elseif ( ! $checked ) { $status = $file_exists ? '✓ ' . esc_html__( 'Generated', 'ai-ready-wp' ) . '' : '✗ ' . esc_html__( 'Missing', 'ai-ready-wp' ) . ''; } else { $settings = AI_Ready_WP::get_settings(); $state = AIREWP_Endpoint_Check::state( $path, $settings ); $note = $file_exists ? esc_html__( 'from file', 'ai-ready-wp' ) : esc_html__( 'built on the fly, no file written', 'ai-ready-wp' ); if ( AIREWP_Endpoint_Check::OK === $state ) { $status = '✓ ' . esc_html__( 'Answering', 'ai-ready-wp' ) . '' . '' . $note . ''; } elseif ( AIREWP_Endpoint_Check::BROKEN === $state ) { $status = '✗ ' . esc_html__( 'Not answering', 'ai-ready-wp' ) . '' . '' . esc_html__( 'the web server does not pass this URL to WordPress', 'ai-ready-wp' ) . ''; } else { $status = '' . esc_html__( 'Not verified', 'ai-ready-wp' ) . '' . '' . $note . ''; } } // Always linked when the feature is on. The URL is the deliverable, and // the old rule (link only when a file exists) hid the one link a site // owner needs to click in exactly the case where something is wrong. $url_col = $enabled ? '' . esc_html( $path ) . '' : '' . esc_html( $path ) . ''; printf( '%s%s%s', esc_html( $label ), wp_kses( $status, $allowed_html ), wp_kses( $url_col, $allowed_html ) ); } /** * URL that drops the cached measurement and reloads this page. */ private function recheck_url(): string { return wp_nonce_url( add_query_arg( [ 'page' => self::PAGE_SLUG, 'airewp_recheck' => '1' ], admin_url( 'options-general.php' ) ), 'airewp_recheck' ); } /** * Handle the "check again" link. */ private function maybe_handle_recheck(): void { if ( empty( $_GET['airewp_recheck'] ) ) { return; } if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'airewp_recheck' ) ) { return; } // Rebuild first, then re-measure. The usual reason for clicking this is // that permissions were just fixed, and measuring before rebuilding // would report the state the site was in a minute ago. AI_Ready_WP::instance()->regenerate_all(); AIREWP_Endpoint_Check::invalidate(); } /** * Discovery paths the last measurement found unreachable. * * @return string[] */ private function broken_endpoints( array $settings ): array { $status = AIREWP_Endpoint_Check::get_status( $settings ); $broken = []; foreach ( $status['paths'] as $path => $result ) { if ( AIREWP_Endpoint_Check::BROKEN === ( $result['state'] ?? '' ) ) { $broken[] = $path; } } return $broken; } /** * Explain a failed write, with the facts needed to fix it. * * Before this the plugin knew perfectly well that a write had failed and * said nothing at all. The status table showed "Missing" and left the site * owner to guess whether that was a bug, a setting, or their server. * * The wording follows the measurement rather than the common case. Naming * .well-known/ unconditionally would be wrong on a site where that * directory is fine and something else is not, and a warning that points at * the wrong directory costs more time than no warning at all. */ private function render_write_warning( bool $any_broken ): void { if ( AI_Ready_WP::writes_ok() ) { return; } $info = AI_Ready_WP::well_known_diagnostics(); // Strictly false, not falsy: null means the transport could not say, // and an unknown is not evidence against this directory. $well_known = $info['exists'] && false === $info['writable']; $target = $well_known ? $info['path'] : AIREWP_ROOT_PATH; // Only worth printing when the two differ. Equal names mean the cause // is something else (the mode, an immutable flag, SELinux, open_basedir) // and pointing at ownership would send the reader down a dead end. $show_users = $well_known && '' !== $info['owner'] && '' !== $info['php_user'] && $info['owner'] !== $info['php_user']; ?>

' . esc_html( $target ) . '' ); ?> ' . esc_html( $info['owner'] ) . '', '' . esc_html( $info['php_user'] ) . '' ); ?>

chown -R