From dd3110b93e03baecd96b4a2db39fbe3dfc58580d Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 22 Feb 2026 10:06:59 +0000 Subject: [PATCH] =?UTF-8?q?Dateien=20nach=20=E2=80=9Ebavarian-rank-engine/?= =?UTF-8?q?includes/Features=E2=80=9C=20hochladen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../includes/Features/CrawlerLog.php | 104 +++++ .../includes/Features/LlmsTxt.php | 217 ++++++++++ .../includes/Features/MetaGenerator.php | 371 ++++++++++++++++++ .../includes/Features/RobotsTxt.php | 51 +++ .../includes/Features/SchemaEnhancer.php | 180 +++++++++ 5 files changed, 923 insertions(+) create mode 100644 bavarian-rank-engine/includes/Features/CrawlerLog.php create mode 100644 bavarian-rank-engine/includes/Features/LlmsTxt.php create mode 100644 bavarian-rank-engine/includes/Features/MetaGenerator.php create mode 100644 bavarian-rank-engine/includes/Features/RobotsTxt.php create mode 100644 bavarian-rank-engine/includes/Features/SchemaEnhancer.php diff --git a/bavarian-rank-engine/includes/Features/CrawlerLog.php b/bavarian-rank-engine/includes/Features/CrawlerLog.php new file mode 100644 index 0000000..e91ec9c --- /dev/null +++ b/bavarian-rank-engine/includes/Features/CrawlerLog.php @@ -0,0 +1,104 @@ +prefix . self::TABLE; + $charset = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + bot_name VARCHAR(64) NOT NULL, + ip_hash VARCHAR(64) NOT NULL DEFAULT '', + url VARCHAR(512) NOT NULL DEFAULT '', + visited_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY bot_name (bot_name), + KEY visited_at (visited_at) + ) {$charset};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $sql ); + } + + public function register(): void { + add_action( 'init', array( $this, 'maybe_log' ), 1 ); + add_action( self::CRON, array( $this, 'purge_old' ) ); + + if ( ! wp_next_scheduled( self::CRON ) ) { + wp_schedule_event( time(), 'weekly', self::CRON ); + } + } + + public function maybe_log(): void { + $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) + ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) + : ''; + if ( empty( $ua ) ) { + return; + } + + $bot = $this->detect_bot( $ua ); + if ( null === $bot ) { + return; + } + + global $wpdb; + $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->prefix . self::TABLE, + array( + 'bot_name' => $bot, + 'ip_hash' => hash( 'sha256', isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ), + 'url' => mb_substr( isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', 0, 512 ), + 'visited_at' => current_time( 'mysql' ), + ), + array( '%s', '%s', '%s', '%s' ) + ); + } + + private function detect_bot( string $ua ): ?string { + foreach ( array_keys( RobotsTxt::KNOWN_BOTS ) as $bot ) { + if ( false !== stripos( $ua, $bot ) ) { + return $bot; + } + } + return null; + } + + public function purge_old(): void { + global $wpdb; + $table = $wpdb->prefix . self::TABLE; + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "DELETE FROM `{$table}` WHERE visited_at < DATE_SUB(NOW(), INTERVAL 90 DAY)" + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared + } + + public static function get_recent_summary( int $days = 30 ): array { + global $wpdb; + if ( ! isset( $wpdb ) ) { + return array(); + } + $table = $wpdb->prefix . self::TABLE; + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql = "SELECT bot_name, COUNT(*) as visits, MAX(visited_at) as last_seen + FROM `{$table}` + WHERE visited_at >= DATE_SUB(NOW(), INTERVAL %d DAY) + GROUP BY bot_name + ORDER BY visits DESC"; + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( $sql, $days ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + ARRAY_A + ) ?: array(); + } +} diff --git a/bavarian-rank-engine/includes/Features/LlmsTxt.php b/bavarian-rank-engine/includes/Features/LlmsTxt.php new file mode 100644 index 0000000..1d83cc5 --- /dev/null +++ b/bavarian-rank-engine/includes/Features/LlmsTxt.php @@ -0,0 +1,217 @@ +serve_page( 1 ); + return; + } + if ( preg_match( '#^/llms-(\d+)\.txt$#', $uri, $m ) ) { + $this->serve_page( (int) $m[1] ); + } + } + + public function rank_math_notice(): void { + if ( ! defined( 'RANK_MATH_VERSION' ) ) { + return; + } + $settings = self::getSettings(); + if ( empty( $settings['enabled'] ) ) { + return; + } + echo '

' + . esc_html__( 'Bavarian Rank Engine bedient llms.txt mit Priorität — kein Handlungsbedarf bei Rank Math.', 'bavarian-rank-engine' ) + . '

'; + } + + public function add_rewrite_rule(): void { + add_rewrite_rule( '^llms\.txt$', 'index.php?bre_llms=1', 'top' ); + } + + public function add_query_var( array $vars ): array { + $vars[] = 'bre_llms'; + return $vars; + } + + private function serve_page( int $page ): void { + $settings = self::getSettings(); + if ( empty( $settings['enabled'] ) ) { + status_header( 404 ); + exit; + } + + $cache_key = self::CACHE_KEY . '_p' . $page; + $cached = get_transient( $cache_key ); + + if ( $cached === false ) { + $cached = $this->build( $settings, $page ); + set_transient( $cache_key, $cached, 0 ); + } + + $etag = '"' . md5( $cached ) . '"'; + $last_modified = $this->get_last_modified(); + + header( 'Content-Type: text/plain; charset=utf-8' ); + header( 'ETag: ' . $etag ); + header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT' ); + header( 'Cache-Control: public, max-age=3600' ); + + $if_none_match = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) : ''; + if ( $if_none_match === $etag ) { + status_header( 304 ); + exit; + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $cached; + exit; + } + + private function get_last_modified(): int { + global $wpdb; + if ( ! isset( $wpdb ) ) { + return time(); + } + $latest = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT UNIX_TIMESTAMP(MAX(post_modified_gmt)) FROM {$wpdb->posts} + WHERE post_status = 'publish'" + ); + return $latest ? (int) $latest : time(); + } + + public static function clear_cache(): void { + global $wpdb; + if ( ! isset( $wpdb ) ) { + return; + } + $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bre_llms_cache%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_bre_llms_cache%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + } + + private function build( array $s, int $page = 1 ): string { + $max_links = max( 50, (int) ( $s['max_links'] ?? 500 ) ); + $post_types = $s['post_types'] ?? array( 'post', 'page' ); + $all_posts = $this->get_all_posts( $post_types ); + $total = count( $all_posts ); + $pages = $total > 0 ? (int) ceil( $total / $max_links ) : 1; + $offset = ( $page - 1 ) * $max_links; + $page_posts = array_slice( $all_posts, $offset, $max_links ); + + $out = ''; + + if ( $page === 1 ) { + if ( ! empty( $s['title'] ) ) { + $out .= '# ' . $s['title'] . "\n\n"; + } + if ( ! empty( $s['description_before'] ) ) { + $out .= trim( $s['description_before'] ) . "\n\n"; + } + if ( ! empty( $s['custom_links'] ) ) { + $out .= "## Featured Resources\n\n"; + foreach ( explode( "\n", trim( $s['custom_links'] ) ) as $line ) { + $line = trim( $line ); + if ( $line !== '' ) { + $out .= $line . "\n"; + } + } + $out .= "\n"; + } + } + + if ( ! empty( $page_posts ) ) { + $out .= "## Content\n\n"; + foreach ( $page_posts as $post ) { + $out .= sprintf( + '- [%s](%s) — %s', + $post->post_title, + get_permalink( $post ), + get_the_date( 'Y-m-d', $post ) + ) . "\n"; + } + $out .= "\n"; + } + + if ( $pages > 1 ) { + $out .= "## More\n\n"; + for ( $p = 1; $p <= $pages; $p++ ) { + if ( $p === $page ) { + continue; + } + $filename = $p === 1 ? 'llms.txt' : "llms-{$p}.txt"; + $url = home_url( '/' . $filename ); + $out .= "- [{$filename}]({$url})\n"; + } + $out .= "\n"; + } + + if ( $page === 1 ) { + if ( ! empty( $s['description_after'] ) ) { + $out .= "\n---\n" . trim( $s['description_after'] ) . "\n"; + } + if ( ! empty( $s['description_footer'] ) ) { + $out .= "\n---\n" . trim( $s['description_footer'] ) . "\n"; + } + } + + return $out; + } + + private function get_all_posts( array $post_types ): array { + if ( empty( $post_types ) ) { + return array(); + } + $query = new \WP_Query( + array( + 'post_type' => $post_types, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + ) + ); + $posts = $query->posts; + wp_reset_postdata(); + return $posts; + } + + /** + * Flush rewrite rules on activation. + * Call this from your activation hook. + */ + public function flush_rules(): void { + $this->add_rewrite_rule(); + flush_rewrite_rules(); + } + + public static function getSettings(): array { + $defaults = array( + 'enabled' => false, + 'title' => '', + 'description_before' => '', + 'description_after' => '', + 'description_footer' => '', + 'custom_links' => '', + 'post_types' => array( 'post', 'page' ), + 'max_links' => 500, + ); + $saved = get_option( self::OPTION_KEY, array() ); + return array_merge( $defaults, is_array( $saved ) ? $saved : array() ); + } +} diff --git a/bavarian-rank-engine/includes/Features/MetaGenerator.php b/bavarian-rank-engine/includes/Features/MetaGenerator.php new file mode 100644 index 0000000..fac7404 --- /dev/null +++ b/bavarian-rank-engine/includes/Features/MetaGenerator.php @@ -0,0 +1,371 @@ +hasExistingMeta( $post_id ) ) { + return; + } + + $settings = SettingsPage::getSettings(); + if ( ! in_array( $post->post_type, $settings['meta_post_types'], true ) ) { + return; + } + + try { + $api_key = $settings['api_keys'][ $settings['provider'] ] ?? ''; + $source = ! empty( $api_key ) ? 'ai' : 'fallback'; + $description = $this->generate( $post, $settings ); + if ( ! empty( $description ) ) { + $this->saveMeta( $post_id, $description, $source ); + } + } catch ( \Exception $e ) { + if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( '[BRE] Meta generation failed for post ' . $post_id . ': ' . $e->getMessage() ); + } + // Try fallback + $fallback = FallbackMeta::extract( $post ); + if ( $fallback !== '' ) { + $this->saveMeta( $post_id, $fallback, 'fallback' ); + } + } + } + + public function generate( \WP_Post $post, array $settings ): string { + $registry = ProviderRegistry::instance(); + $provider = $registry->get( $settings['provider'] ); + $api_key = $settings['api_keys'][ $settings['provider'] ] ?? ''; + + // No provider or no API key → use fallback immediately + if ( ! $provider || empty( $api_key ) ) { + return FallbackMeta::extract( $post ); + } + + $model = $settings['models'][ $settings['provider'] ] ?? array_key_first( $provider->getModels() ); + $content = $this->prepareContent( $post, $settings ); + $prompt = $this->buildPrompt( $post, $content, $settings ); + + return $provider->generateText( $prompt, $api_key, $model, 300 ); + } + + private function prepareContent( \WP_Post $post, array $settings ): string { + $content = wp_strip_all_tags( $post->post_content ); + if ( $settings['token_mode'] === 'limit' ) { + $content = TokenEstimator::truncate( $content, (int) $settings['token_limit'] ); + } + return $content; + } + + private function buildPrompt( \WP_Post $post, string $content, array $settings ): string { + $language = $this->detectLanguage( $post ); + $prompt = $settings['prompt']; + + $prompt = str_replace( '{title}', $post->post_title, $prompt ); + $prompt = str_replace( '{content}', $content, $prompt ); + $prompt = str_replace( '{excerpt}', $post->post_excerpt ?: '', $prompt ); + $prompt = str_replace( '{language}', $language, $prompt ); + + return apply_filters( 'bre_prompt', $prompt, $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + } + + private function detectLanguage( \WP_Post $post ): string { + if ( function_exists( 'pll_get_post_language' ) ) { + $lang = pll_get_post_language( $post->ID, 'name' ); + if ( $lang ) { + return $lang; + } + } + + if ( defined( 'ICL_LANGUAGE_CODE' ) ) { + return ICL_LANGUAGE_CODE; + } + + $locale_map = array( + 'de_DE' => 'Deutsch', + 'de_AT' => 'Deutsch', + 'de_CH' => 'Deutsch', + 'en_US' => 'English', + 'en_GB' => 'English', + 'fr_FR' => 'Français', + 'es_ES' => 'Español', + ); + + return $locale_map[ get_locale() ] ?? 'Deutsch'; + } + + public function hasExistingMeta( int $post_id ): bool { + $fields = array( + '_bre_meta_description', + 'rank_math_description', + '_yoast_wpseo_metadesc', + '_aioseo_description', + '_seopress_titles_desc', + '_meta_description', + ); + foreach ( $fields as $field ) { + if ( ! empty( get_post_meta( $post_id, $field, true ) ) ) { + return true; + } + } + return false; + } + + public function saveMeta( int $post_id, string $description, string $source = 'ai' ): void { + $clean = sanitize_text_field( $description ); + update_post_meta( $post_id, '_bre_meta_source', sanitize_key( $source ) ); + update_post_meta( $post_id, '_bre_meta_description', $clean ); + + if ( defined( 'RANK_MATH_VERSION' ) ) { + update_post_meta( $post_id, 'rank_math_description', $clean ); + } elseif ( defined( 'WPSEO_VERSION' ) ) { + update_post_meta( $post_id, '_yoast_wpseo_metadesc', $clean ); + } elseif ( defined( 'AIOSEO_VERSION' ) ) { + update_post_meta( $post_id, '_aioseo_description', $clean ); + } elseif ( class_exists( 'SeoPress_Titles_Admin' ) ) { + update_post_meta( $post_id, '_seopress_titles_desc', $clean ); + } + + do_action( 'bre_meta_saved', $post_id, $description ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + } + + public function ajaxBulkStats(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( __( 'Insufficient permissions.', 'bavarian-rank-engine' ) ); + } + + $settings = SettingsPage::getSettings(); + $stats = array(); + + foreach ( $settings['meta_post_types'] as $pt ) { + $stats[ $pt ] = $this->countPostsWithoutMeta( $pt ); + } + + wp_send_json_success( $stats ); + } + + public function ajaxBulkRelease(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error(); + } + BulkQueue::release(); + wp_send_json_success(); + } + + public function ajaxBulkStatus(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error(); + } + wp_send_json_success( + array( + 'locked' => BulkQueue::isLocked(), + 'lock_age' => BulkQueue::lockAge(), + ) + ); + } + + public function ajaxBulkGenerate(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( __( 'Insufficient permissions.', 'bavarian-rank-engine' ) ); + } + + // Acquire lock on first batch + if ( ! empty( $_POST['is_first'] ) ) { + if ( ! BulkQueue::acquire() ) { + wp_send_json_error( + array( + 'locked' => true, + 'lock_age' => BulkQueue::lockAge(), + 'message' => __( 'Ein Bulk-Prozess läuft bereits.', 'bavarian-rank-engine' ), + ) + ); + return; + } + } + + $post_type = sanitize_key( wp_unslash( $_POST['post_type'] ?? 'post' ) ); + $limit = min( 20, max( 1, absint( wp_unslash( $_POST['batch_size'] ?? 5 ) ) ) ); + $settings = SettingsPage::getSettings(); + + if ( ! empty( $_POST['provider'] ) ) { + $settings['provider'] = sanitize_key( wp_unslash( $_POST['provider'] ) ); + } + if ( ! empty( $_POST['model'] ) ) { + $provider_obj = ProviderRegistry::instance()->get( $settings['provider'] ); + $allowed_models = $provider_obj ? array_keys( $provider_obj->getModels() ) : array(); + $requested_model = sanitize_text_field( wp_unslash( $_POST['model'] ) ); + if ( in_array( $requested_model, $allowed_models, true ) ) { + $settings['models'][ $settings['provider'] ] = $requested_model; + } + } + + $post_ids = $this->getPostsWithoutMeta( $post_type, $limit ); + $results = array(); + $max_retries = 3; + + foreach ( $post_ids as $post_id ) { + $post = get_post( $post_id ); + $success = false; + $last_error = ''; + + for ( $attempt = 1; $attempt <= $max_retries; $attempt++ ) { + try { + $desc = $this->generate( $post, $settings ); + $this->saveMeta( $post_id, $desc ); + delete_post_meta( $post_id, '_bre_bulk_failed' ); + $results[] = array( + 'id' => $post_id, + 'title' => get_the_title( $post_id ), + 'description' => $desc, + 'success' => true, + 'attempts' => $attempt, + ); + $success = true; + break; + } catch ( \Exception $e ) { + $last_error = $e->getMessage(); + if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( '[BRE] Post ' . $post_id . ' attempt ' . $attempt . '/' . $max_retries . ': ' . $last_error ); + } + if ( $attempt < $max_retries ) { + sleep( 1 ); + } + } + } + + if ( ! $success ) { + update_post_meta( $post_id, '_bre_bulk_failed', $last_error ); + $results[] = array( + 'id' => $post_id, + 'title' => get_the_title( $post_id ), + 'error' => $last_error, + 'success' => false, + ); + } + } + + // Release lock when JS signals last batch + if ( ! empty( $_POST['is_last'] ) ) { + BulkQueue::release(); + } + + wp_send_json_success( + array( + 'results' => $results, + 'processed' => count( $results ), + 'remaining' => $this->countPostsWithoutMeta( $post_type ), + 'locked' => BulkQueue::isLocked(), + ) + ); + } + + private function countPostsWithoutMeta( string $post_type ): int { + global $wpdb; + + $meta_fields = array( + '_bre_meta_description', + 'rank_math_description', + '_yoast_wpseo_metadesc', + '_aioseo_description', + '_seopress_titles_desc', + '_meta_description', + ); + + $not_exists = ''; + foreach ( $meta_fields as $field ) { + $not_exists .= $wpdb->prepare( + " AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} pm + WHERE pm.post_id = p.ID AND pm.meta_key = %s AND pm.meta_value != '' + )", + $field + ); + } + + return (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->posts} p + WHERE p.post_type = %s AND p.post_status = 'publish'" . $not_exists, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $post_type + ) + ); + } + + private function getPostsWithoutMeta( string $post_type, int $limit ): array { + global $wpdb; + + $meta_fields = array( + '_bre_meta_description', + 'rank_math_description', + '_yoast_wpseo_metadesc', + '_aioseo_description', + '_seopress_titles_desc', + '_meta_description', + ); + + $not_exists = ''; + foreach ( $meta_fields as $field ) { + $not_exists .= $wpdb->prepare( + " AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} pm + WHERE pm.post_id = p.ID + AND pm.meta_key = %s + AND pm.meta_value != '' + )", + $field + ); + } + + return array_map( + 'intval', + $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + "SELECT p.ID FROM {$wpdb->posts} p + WHERE p.post_type = %s AND p.post_status = 'publish'" + . $not_exists . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + ' ORDER BY p.ID DESC LIMIT %d', + $post_type, + $limit + ) + ) + ); + } +} diff --git a/bavarian-rank-engine/includes/Features/RobotsTxt.php b/bavarian-rank-engine/includes/Features/RobotsTxt.php new file mode 100644 index 0000000..3361f3f --- /dev/null +++ b/bavarian-rank-engine/includes/Features/RobotsTxt.php @@ -0,0 +1,51 @@ + 'OpenAI GPTBot', + 'ClaudeBot' => 'Anthropic ClaudeBot', + 'Google-Extended' => 'Google Extended (Bard/Gemini Training)', + 'PerplexityBot' => 'Perplexity AI', + 'CCBot' => 'Common Crawl (CCBot)', + 'Applebot-Extended' => 'Apple AI (Applebot-Extended)', + 'Bytespider' => 'ByteDance Bytespider', + 'DataForSeoBot' => 'DataForSEO Bot', + 'ImagesiftBot' => 'Imagesift Bot', + 'omgili' => 'Omgili Bot', + 'Diffbot' => 'Diffbot', + 'FacebookBot' => 'Meta FacebookBot', + 'Amazonbot' => 'Amazon Amazonbot', + ); + + public function register(): void { + add_filter( 'robots_txt', array( $this, 'append_rules' ), 20, 2 ); + } + + public function append_rules( string $output, bool $public ): string { + $settings = self::getSettings(); + $blocked = $settings['blocked_bots'] ?? array(); + + foreach ( $blocked as $bot ) { + if ( isset( self::KNOWN_BOTS[ $bot ] ) ) { + $output .= "\nUser-agent: {$bot}\nDisallow: /\n"; + } + } + + return $output; + } + + public static function getSettings(): array { + $saved = get_option( self::OPTION_KEY, array() ); + return array_merge( + array( 'blocked_bots' => array() ), + is_array( $saved ) ? $saved : array() + ); + } +} diff --git a/bavarian-rank-engine/includes/Features/SchemaEnhancer.php b/bavarian-rank-engine/includes/Features/SchemaEnhancer.php new file mode 100644 index 0000000..5756c8d --- /dev/null +++ b/bavarian-rank-engine/includes/Features/SchemaEnhancer.php @@ -0,0 +1,180 @@ +' . "\n"; + echo '' . "\n"; + } + + public function outputMetaDescription(): void { + if ( defined( 'RANK_MATH_VERSION' ) || defined( 'WPSEO_VERSION' ) || defined( 'AIOSEO_VERSION' ) ) { + return; + } + if ( ! is_singular() ) { + return; + } + + $desc = get_post_meta( get_the_ID(), '_bre_meta_description', true ); + if ( empty( $desc ) ) { + return; + } + + echo '' . "\n"; + } + + public function outputJsonLd(): void { + $settings = SettingsPage::getSettings(); + $enabled = $settings['schema_enabled'] ?? array(); + $schemas = array(); + + if ( in_array( 'organization', $enabled, true ) ) { + $schemas[] = $this->buildOrganizationSchema( $settings ); + } + + if ( is_singular() ) { + if ( in_array( 'article_about', $enabled, true ) ) { + $schemas[] = $this->buildArticleSchema(); + } + if ( in_array( 'author', $enabled, true ) ) { + $schemas[] = $this->buildAuthorSchema(); + } + if ( in_array( 'speakable', $enabled, true ) ) { + $schemas[] = $this->buildSpeakableSchema(); + } + } + + if ( in_array( 'breadcrumb', $enabled, true ) + && ! defined( 'RANK_MATH_VERSION' ) + && ! defined( 'WPSEO_VERSION' ) ) { + $breadcrumb = $this->buildBreadcrumbSchema(); + if ( $breadcrumb ) { + $schemas[] = $breadcrumb; + } + } + + foreach ( $schemas as $schema ) { + echo '' . "\n"; + } + } + + private function buildOrganizationSchema( array $settings ): array { + $same_as = array_values( array_filter( $settings['schema_same_as']['organization'] ?? array() ) ); + $schema = array( + '@context' => 'https://schema.org', + '@type' => 'Organization', + 'name' => get_bloginfo( 'name' ), + 'url' => home_url( '/' ), + ); + if ( ! empty( $same_as ) ) { + $schema['sameAs'] = $same_as; + } + $logo = get_site_icon_url( 192 ); + if ( $logo ) { + $schema['logo'] = $logo; + } + return $schema; + } + + private function buildArticleSchema(): array { + return array( + '@context' => 'https://schema.org', + '@type' => 'Article', + 'headline' => get_the_title(), + 'url' => get_permalink(), + 'datePublished' => get_the_date( 'c' ), + 'dateModified' => get_the_modified_date( 'c' ), + 'description' => get_post_meta( get_the_ID(), '_bre_meta_description', true ) ?: get_the_excerpt(), + 'publisher' => array( + '@type' => 'Organization', + 'name' => get_bloginfo( 'name' ), + 'url' => home_url( '/' ), + ), + ); + } + + private function buildAuthorSchema(): array { + $author_id = (int) get_the_author_meta( 'ID' ); + $schema = array( + '@context' => 'https://schema.org', + '@type' => 'Person', + 'name' => get_the_author(), + 'url' => get_author_posts_url( $author_id ), + ); + $twitter = get_the_author_meta( 'twitter', $author_id ); + if ( $twitter ) { + $schema['sameAs'] = array( 'https://twitter.com/' . ltrim( $twitter, '@' ) ); + } + return $schema; + } + + private function buildSpeakableSchema(): array { + return array( + '@context' => 'https://schema.org', + '@type' => 'WebPage', + 'url' => get_permalink(), + 'speakable' => array( + '@type' => 'SpeakableSpecification', + 'cssSelector' => array( 'h1', '.entry-content p:first-of-type', '.post-content p:first-of-type' ), + ), + ); + } + + private function buildBreadcrumbSchema(): ?array { + if ( ! is_singular() && ! is_category() ) { + return null; + } + + $items = array( + array( + '@type' => 'ListItem', + 'position' => 1, + 'name' => get_bloginfo( 'name' ), + 'item' => home_url( '/' ), + ), + ); + + if ( is_singular() ) { + $items[] = array( + '@type' => 'ListItem', + 'position' => 2, + 'name' => get_the_title(), + 'item' => get_permalink(), + ); + } + + return array( + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => $items, + ); + } +}