*/ public static function defaults(): array { return array( 'verify_google' => '', 'verify_bing' => '', 'verify_pinterest' => '', 'verify_yandex' => '', 'verify_baidu' => '', 'verify_naver' => '', 'verify_facebook' => '', 'indexing_auto' => false, 'indexing_types' => array( 'post', 'page' ), ); } public function register( Container $container ): void { $container->share( Services\IndexingApi::class, static fn() => new Services\IndexingApi() ); } public function boot( Container $container ): void { add_action( 'rest_api_init', array( $this, 'routes' ) ); Guard::action( 'wp_head', array( $this, 'verification_tags' ), 2 ); add_action( 'transition_post_status', array( $this, 'auto_submit' ), 10, 3 ); } /** * Output search engine verification meta tags on the front page only — * that is all any of these services read. */ public function verification_tags(): void { if ( ! is_front_page() && ! is_home() ) { return; } $settings = $this->settings(); $map = array( 'verify_google' => 'google-site-verification', 'verify_bing' => 'msvalidate.01', 'verify_pinterest' => 'p:domain_verify', 'verify_yandex' => 'yandex-verification', // Baidu and Naver matter wherever Google is not the default engine, // and both feed AI answers in their own markets. 'verify_baidu' => 'baidu-site-verification', 'verify_naver' => 'naver-site-verification', 'verify_facebook' => 'facebook-domain-verification', ); foreach ( $map as $key => $name ) { $value = trim( (string) ( $settings[ $key ] ?? '' ) ); if ( '' === $value ) { continue; } printf( '' . "\n", esc_attr( $name ), esc_attr( $value ) ); } } /** * Submit a URL to Google when a post is published or updated. * * @param string $new_status New status. * @param string $old_status Old status. * @param WP_Post $post The post. */ public function auto_submit( string $new_status, string $old_status, $post ): void { if ( ! $post instanceof WP_Post ) { return; } $settings = $this->settings(); if ( empty( $settings['indexing_auto'] ) ) { return; } if ( ! in_array( $post->post_type, (array) $settings['indexing_types'], true ) ) { return; } if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) { return; } $api = \AiRankSuite\Kernel::service( Services\IndexingApi::class ); if ( ! $api->is_configured() ) { return; } if ( 'publish' === $new_status ) { $api->publish( (string) get_permalink( $post ), 'URL_UPDATED' ); return; } if ( 'publish' === $old_status && in_array( $new_status, array( 'trash', 'draft', 'private' ), true ) ) { $api->publish( (string) get_permalink( $post ), 'URL_DELETED' ); } } public function routes(): void { register_rest_route( self::NAMESPACE, '/settings/google', 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( 'verify_google' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_bing' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_pinterest' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_yandex' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_baidu' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_naver' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'verify_facebook' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'indexing_auto' => array( 'type' => 'boolean' ), 'indexing_types' => array( 'type' => 'array' ), ), ), ) ); register_rest_route( self::NAMESPACE, '/google/indexing/account', array( array( 'methods' => 'POST', 'callback' => array( $this, 'save_account' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'json' => array( 'type' => 'string', 'required' => true, ), ), ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'clear_account' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ), ) ); register_rest_route( self::NAMESPACE, '/google/rich-results', array( 'methods' => 'GET', 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'callback' => static fn( WP_REST_Request $request ) => new WP_REST_Response( Services\RichResults::scan( max( 20, min( 500, (int) $request->get_param( 'limit' ) ) ), (bool) $request->get_param( 'fresh' ) ) ), 'args' => array( 'limit' => array( 'type' => 'integer', 'default' => 100 ), 'fresh' => array( 'type' => 'boolean', 'default' => false ), ), ) ); register_rest_route( self::NAMESPACE, '/google/indexing/submit', array( 'methods' => 'POST', 'callback' => array( $this, 'submit' ), 'permission_callback' => static function () { if ( ! current_user_can( 'manage_options' ) ) { return self::forbidden(); } return self::spend( 'indexing', 60 ); }, 'args' => array( 'post_id' => array( 'type' => 'integer' ), 'url' => array( 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', ), 'type' => array( 'type' => 'string', 'default' => 'URL_UPDATED', 'enum' => array( 'URL_UPDATED', 'URL_DELETED' ), ), ), ) ); } public function get_settings(): WP_REST_Response { $api = \AiRankSuite\Kernel::service( Services\IndexingApi::class ); return new WP_REST_Response( array( 'settings' => $this->settings(), 'indexing' => array( 'configured' => $api->is_configured(), 'email' => $api->client_email(), 'used_today' => $api->used_today(), 'quota' => $api->quota(), 'recent' => $api->recent(), ), /* * Slug and label. The screen used to print the slug, so a site * with custom types offered "unterkunftsarten" and "sso_places" * as things to tick — the reader has to translate back to what * they see in the menu. WordPress already knows the name. */ 'post_types' => array_values( array_map( static function ( string $slug ): array { $object = get_post_type_object( $slug ); return array( 'slug' => $slug, 'label' => ( $object && ! empty( $object->labels->name ) ) ? (string) $object->labels->name : $slug, ); }, array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) ) ) ), ) ); } 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 ) ) { continue; } if ( 'indexing_types' === $key ) { $public = get_post_types( array( 'public' => true ) ); $values[ $key ] = array_values( array_intersect( (array) $request->get_param( $key ), $public ) ); continue; } $values[ $key ] = $request->get_param( $key ); } $this->options()->set( 'google', $values, $defaults ); return $this->get_settings(); } public function save_account( WP_REST_Request $request ): WP_REST_Response|WP_Error { $api = \AiRankSuite\Kernel::service( Services\IndexingApi::class ); $result = $api->save_account( (string) $request->get_param( 'json' ) ); return $result instanceof WP_Error ? $result : $this->get_settings(); } public function clear_account(): WP_REST_Response { \AiRankSuite\Kernel::service( Services\IndexingApi::class )->clear_account(); return $this->get_settings(); } public function submit( WP_REST_Request $request ): WP_REST_Response|WP_Error { $url = (string) $request->get_param( 'url' ); if ( '' === $url && $request->get_param( 'post_id' ) ) { // The permalink of a post this person cannot read is not theirs to // look up, let alone to announce to Google. if ( ! self::post_allowed( (int) $request->get_param( 'post_id' ), 'read_post' ) ) { return self::forbidden(); } $url = (string) get_permalink( (int) $request->get_param( 'post_id' ) ); } if ( '' === $url ) { return new WP_Error( 'airanksuite_indexing_no_url', __( 'No URL to submit.', 'ai-rank-suite' ), array( 'status' => 400 ) ); } $api = \AiRankSuite\Kernel::service( Services\IndexingApi::class ); $result = $api->publish( $url, (string) $request->get_param( 'type' ) ); return $result instanceof WP_Error ? $result : $this->get_settings(); } /** @return array */ private function settings(): array { return $this->options()->get( 'google', self::defaults() ); } private function options(): Options { return \AiRankSuite\Kernel::service( Options::class ); } }