*/ public const DIRECTIVES = array( 'noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex' ); public function id(): string { return 'robots'; } /** @return array */ public static function defaults(): array { return array( 'noindex_author' => false, 'noindex_date' => true, 'noindex_search' => true, 'noindex_paginated' => false, 'noindex_attachment' => true, 'noindex_password' => true, 'attachment_parent' => true, // Redirect attachment pages to their parent post. 'max_snippet' => -1, 'max_image_preview' => 'large', 'max_video_preview' => -1, 'robots_txt_custom' => '', 'robots_txt_enabled' => false, ); } public function register( Container $container ): void {} public function boot( Container $container ): void { add_action( 'rest_api_init', array( $this, 'routes' ) ); Guard::filter( 'wp_robots', array( $this, 'robots' ), 20 ); Guard::action( 'template_redirect', array( $this, 'attachment_redirect' ), 2 ); Guard::filter( 'robots_txt', array( $this, 'robots_txt' ), 30 ); } /** * Merge our directives into WordPress' robots meta tag. * * @param array $robots Existing directives. * @return array */ public function robots( array $robots ): array { $settings = $this->settings(); if ( is_singular() ) { $post = get_queried_object(); if ( $post instanceof \WP_Post ) { $stored = get_post_meta( $post->ID, '_airanksuite_robots', true ); $stored = is_array( $stored ) ? $stored : array(); foreach ( self::DIRECTIVES as $directive ) { if ( in_array( $directive, $stored, true ) ) { $robots[ $directive ] = true; if ( 'noindex' === $directive ) { unset( $robots['index'] ); } } } } } // Password-protected content must not appear in search results or // AI training sets — the content behind it is not public. if ( is_singular() && ! empty( $settings['noindex_password'] ) && post_password_required() ) { $robots['noindex'] = true; unset( $robots['index'] ); } $archive_rules = array( 'noindex_author' => is_author(), 'noindex_date' => is_date(), 'noindex_search' => is_search(), 'noindex_attachment' => is_attachment(), 'noindex_paginated' => is_paged(), ); foreach ( $archive_rules as $key => $matches ) { if ( $matches && ! empty( $settings[ $key ] ) ) { $robots['noindex'] = true; unset( $robots['index'] ); } } // Rich-result hints that help both search engines and AI crawlers. // The values must be strings: wp_robots() only renders `directive:value` // for strings and collapses anything else to a bare directive, which // Google then ignores. if ( empty( $robots['noindex'] ) ) { $robots['max-snippet'] = (string) (int) $settings['max_snippet']; $robots['max-image-preview'] = (string) $settings['max_image_preview']; if ( isset( $settings['max_video_preview'] ) && '' !== (string) $settings['max_video_preview'] ) { $robots['max-video-preview'] = (string) (int) $settings['max_video_preview']; } // A per-post override always wins over the global default. if ( is_singular() ) { $post = get_queried_object(); if ( $post instanceof \WP_Post ) { $previews = \AiRankSuite\Support\PostSeo::previews( $post->ID ); if ( isset( $previews['max_snippet'] ) ) { $robots['max-snippet'] = (string) (int) $previews['max_snippet']; } if ( isset( $previews['max_image_preview'] ) ) { $robots['max-image-preview'] = $previews['max_image_preview']; } if ( isset( $previews['max_video_preview'] ) ) { $robots['max-video-preview'] = (string) (int) $previews['max_video_preview']; } } } } return $robots; } /** * Attachment pages are thin duplicates — send visitors to the real content. */ public function attachment_redirect(): void { if ( ! is_attachment() || empty( $this->settings()['attachment_parent'] ) ) { return; } $post = get_queried_object(); $parent = $post instanceof \WP_Post ? (int) $post->post_parent : 0; $target = $parent > 0 && 'publish' === get_post_status( $parent ) ? (string) get_permalink( $parent ) : (string) wp_get_attachment_url( $post->ID ?? 0 ); if ( '' !== $target ) { wp_safe_redirect( $target, 301 ); exit; } } /** * Append fully custom robots.txt lines (after the AI crawler block). */ public function robots_txt( string $output ): string { $settings = $this->settings(); if ( empty( $settings['robots_txt_enabled'] ) ) { return $output; } $custom = trim( (string) $settings['robots_txt_custom'] ); if ( '' === $custom ) { return $output; } return $output . "\n# Custom rules (AI Rank Suite)\n" . $custom . "\n"; } public function routes(): void { register_rest_route( self::NAMESPACE, '/settings/robots', array( array( 'methods' => 'GET', 'callback' => array( $this, 'get_settings' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ), array( 'methods' => 'PUT', 'callback' => array( $this, 'update_settings' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'noindex_author' => array( 'type' => 'boolean' ), 'noindex_date' => array( 'type' => 'boolean' ), 'noindex_search' => array( 'type' => 'boolean' ), 'noindex_paginated' => array( 'type' => 'boolean' ), 'noindex_attachment' => array( 'type' => 'boolean' ), 'noindex_password' => array( 'type' => 'boolean' ), 'attachment_parent' => array( 'type' => 'boolean' ), 'max_snippet' => array( 'type' => 'integer' ), 'max_image_preview' => array( 'type' => 'string', 'enum' => array( 'none', 'standard', 'large' ), ), 'max_video_preview' => array( 'type' => 'integer' ), 'robots_txt_enabled' => array( 'type' => 'boolean' ), 'robots_txt_custom' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field', ), ), ), ) ); } public function get_settings(): WP_REST_Response { $physical = file_exists( trailingslashit( ABSPATH ) . 'robots.txt' ); return new WP_REST_Response( array( 'settings' => $this->settings(), 'robots_url' => home_url( '/robots.txt' ), 'physical_robots_txt' => $physical, 'directives' => self::DIRECTIVES, ) ); } public function update_settings( WP_REST_Request $request ): WP_REST_Response { $defaults = self::defaults(); $values = array(); foreach ( array_keys( $defaults ) as $key ) { if ( $request->has_param( $key ) ) { $values[ $key ] = $request->get_param( $key ); } } \AiRankSuite\Kernel::service( Options::class )->set( 'robots', $values, $defaults ); return $this->get_settings(); } /** @return array */ private function settings(): array { return \AiRankSuite\Kernel::service( Options::class )->get( 'robots', self::defaults() ); } }