diff --git a/bavarian-rank-engine/includes/Admin/AdminMenu.php b/bavarian-rank-engine/includes/Admin/AdminMenu.php new file mode 100644 index 0000000..83cc86a --- /dev/null +++ b/bavarian-rank-engine/includes/Admin/AdminMenu.php @@ -0,0 +1,121 @@ +get_meta_stats( $post_types ); + + include BRE_DIR . 'includes/Admin/views/dashboard.php'; + } + + private function get_meta_stats( array $post_types ): array { + global $wpdb; + $stats = array(); + foreach ( $post_types as $pt ) { + $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'", + $pt + ) + ); + $with_meta = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID + WHERE p.post_type = %s AND p.post_status = 'publish' + AND pm.meta_key = %s AND pm.meta_value != ''", + $pt, + '_bre_meta_description' + ) + ); + $stats[ $pt ] = array( + 'total' => $total, + 'with_meta' => $with_meta, + 'pct' => $total > 0 ? round( ( $with_meta / $total ) * 100 ) : 0, + ); + } + return $stats; + } +} diff --git a/bavarian-rank-engine/includes/Admin/BulkPage.php b/bavarian-rank-engine/includes/Admin/BulkPage.php new file mode 100644 index 0000000..c73b03c --- /dev/null +++ b/bavarian-rank-engine/includes/Admin/BulkPage.php @@ -0,0 +1,45 @@ + wp_create_nonce( 'bre_admin' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'isLocked' => \BavarianRankEngine\Helpers\BulkQueue::isLocked(), + 'lockAge' => \BavarianRankEngine\Helpers\BulkQueue::lockAge(), + 'rateDelay' => 6000, + 'costs' => $settings['costs'] ?? array(), + ) + ); + } + + public function render(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + $settings = SettingsPage::getSettings(); + $registry = ProviderRegistry::instance(); + $providers = $registry->all(); + include BRE_DIR . 'includes/Admin/views/bulk.php'; + } +} diff --git a/bavarian-rank-engine/includes/Admin/LinkAnalysis.php b/bavarian-rank-engine/includes/Admin/LinkAnalysis.php new file mode 100644 index 0000000..b752ea0 --- /dev/null +++ b/bavarian-rank-engine/includes/Admin/LinkAnalysis.php @@ -0,0 +1,119 @@ + $this->posts_without_internal_links(), + 'too_many_external' => $this->posts_with_many_external_links( $threshold ), + 'pillar_pages' => $this->top_pillar_pages( 5 ), + 'threshold' => $threshold, + ); + + set_transient( self::CACHE_KEY, $data, self::CACHE_TTL ); + wp_send_json_success( $data ); + } + + private function posts_without_internal_links(): array { + global $wpdb; + $site = esc_sql( rtrim( home_url(), '/' ) ); + $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT ID, post_title FROM {$wpdb->posts} + WHERE post_status = 'publish' + AND post_type IN ('post','page') + AND post_content NOT LIKE %s + ORDER BY post_date DESC + LIMIT 20", + '%href="' . $site . '%' + ) + ); + return array_map( + fn( $r ) => array( + 'id' => (int) $r->ID, + 'title' => $r->post_title, + ), + $results + ); + } + + private function posts_with_many_external_links( int $threshold ): array { + global $wpdb; + $host = wp_parse_url( home_url(), PHP_URL_HOST ); + $posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT ID, post_title, post_content FROM {$wpdb->posts} + WHERE post_status = 'publish' AND post_type IN ('post','page') + ORDER BY post_date DESC LIMIT 200" + ); + $over = array(); + foreach ( $posts as $post ) { + preg_match_all( '/href="https?:\/\/([^"\/]+)/', $post->post_content, $m ); + $external = array_filter( $m[1], fn( $h ) => $h !== $host ); + $count = count( $external ); + if ( $count >= $threshold ) { + $over[] = array( + 'id' => (int) $post->ID, + 'title' => $post->post_title, + 'count' => $count, + ); + } + } + usort( $over, fn( $a, $b ) => $b['count'] <=> $a['count'] ); + return array_slice( $over, 0, 20 ); + } + + private function top_pillar_pages( int $top ): array { + global $wpdb; + $site = rtrim( home_url(), '/' ); + $posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + "SELECT post_content FROM {$wpdb->posts} + WHERE post_status = 'publish' AND post_type IN ('post','page')" + ); + $counts = array(); + foreach ( $posts as $post ) { + preg_match_all( + '/href="(' . preg_quote( $site, '/' ) . '[^"]+)"/', + $post->post_content, + $m + ); + foreach ( $m[1] as $url ) { + $url = rtrim( $url, '/' ); + $counts[ $url ] = ( $counts[ $url ] ?? 0 ) + 1; + } + } + arsort( $counts ); + $result = array(); + foreach ( array_slice( $counts, 0, $top, true ) as $url => $count ) { + $result[] = array( + 'url' => $url, + 'count' => $count, + ); + } + return $result; + } +} diff --git a/bavarian-rank-engine/includes/Admin/LlmsPage.php b/bavarian-rank-engine/includes/Admin/LlmsPage.php new file mode 100644 index 0000000..9015d23 --- /dev/null +++ b/bavarian-rank-engine/includes/Admin/LlmsPage.php @@ -0,0 +1,78 @@ + array( $this, 'sanitize' ), + ) + ); + } + + public function enqueue_assets( string $hook ): void { + if ( $hook !== 'bavarian-rank_page_bre-llms' ) { + return; + } + wp_enqueue_style( 'bre-admin', BRE_URL . 'assets/admin.css', array(), BRE_VERSION ); + } + + public function sanitize( mixed $input ): array { + $input = is_array( $input ) ? $input : array(); + $clean = array(); + + $clean['enabled'] = ! empty( $input['enabled'] ); + $clean['title'] = sanitize_text_field( $input['title'] ?? '' ); + $clean['description_before'] = sanitize_textarea_field( $input['description_before'] ?? '' ); + $clean['description_after'] = sanitize_textarea_field( $input['description_after'] ?? '' ); + $clean['description_footer'] = sanitize_textarea_field( $input['description_footer'] ?? '' ); + $clean['custom_links'] = sanitize_textarea_field( $input['custom_links'] ?? '' ); + + $all_post_types = array_keys( get_post_types( array( 'public' => true ) ) ); + $clean['post_types'] = array_values( + array_intersect( + array_map( 'sanitize_key', (array) ( $input['post_types'] ?? array() ) ), + $all_post_types + ) + ); + + $clean['max_links'] = max( 50, (int) ( $input['max_links'] ?? 500 ) ); + + LlmsTxt::clear_cache(); + + return $clean; + } + + public function ajax_clear_cache(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error(); + } + \BavarianRankEngine\Features\LlmsTxt::clear_cache(); + wp_send_json_success( __( 'Cache geleert.', 'bavarian-rank-engine' ) ); + } + + public function render(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + $settings = LlmsTxt::getSettings(); + $post_types = get_post_types( array( 'public' => true ), 'objects' ); + $llms_url = home_url( '/llms.txt' ); + include BRE_DIR . 'includes/Admin/views/llms.php'; + } +} diff --git a/bavarian-rank-engine/includes/Admin/MetaEditorBox.php b/bavarian-rank-engine/includes/Admin/MetaEditorBox.php new file mode 100644 index 0000000..db92570 --- /dev/null +++ b/bavarian-rank-engine/includes/Admin/MetaEditorBox.php @@ -0,0 +1,141 @@ +ID, '_bre_meta_description', true ) ?: ''; + $source = get_post_meta( $post->ID, '_bre_meta_source', true ) ?: 'none'; + + $source_labels = array( + 'ai' => __( 'KI generiert', 'bavarian-rank-engine' ), + 'fallback' => __( 'Fallback (erster Absatz)', 'bavarian-rank-engine' ), + 'manual' => __( 'Manuell bearbeitet', 'bavarian-rank-engine' ), + 'none' => __( 'Noch nicht generiert', 'bavarian-rank-engine' ), + ); + + $settings = SettingsPage::getSettings(); + $api_key = $settings['api_keys'][ $settings['provider'] ] ?? ''; + $has_key = ! empty( $api_key ); + + wp_nonce_field( 'bre_save_meta_' . $post->ID, 'bre_meta_nonce' ); + ?> +

+ + + +

+ +

+ + / 160 + + + + +

+ saveMeta( + $post_id, + sanitize_textarea_field( wp_unslash( $_POST['bre_meta_description'] ) ), + 'manual' + ); + } + + public function enqueue( string $hook ): void { + if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) { + return; + } + wp_enqueue_script( + 'bre-editor-meta', + BRE_URL . 'assets/editor-meta.js', + array( 'jquery' ), + BRE_VERSION, + true + ); + } + + public function ajax_regen(): void { + check_ajax_referer( 'bre_admin', 'nonce' ); + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( 'Insufficient permissions' ); + return; + } + + $post_id = absint( wp_unslash( $_POST['post_id'] ?? 0 ) ); + $post = $post_id ? get_post( $post_id ) : null; + if ( ! $post ) { + wp_send_json_error( 'Post not found' ); + return; + } + + $settings = SettingsPage::getSettings(); + $gen = new MetaGenerator(); + + try { + $desc = $gen->generate( $post, $settings ); + $gen->saveMeta( $post_id, $desc, 'ai' ); + wp_send_json_success( array( 'description' => $desc ) ); + } catch ( \Exception $e ) { + wp_send_json_error( $e->getMessage() ); + } + } +}