Dateien nach „bavarian-rank-engine/includes/Admin“ hochladen

This commit is contained in:
Michael Fuchs 2026-02-22 10:06:25 +00:00
parent d249a1f1ee
commit 30cc5ed244
5 changed files with 504 additions and 0 deletions

View file

@ -0,0 +1,121 @@
<?php
namespace BavarianRankEngine\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class AdminMenu {
public function register(): void {
add_action( 'admin_menu', array( $this, 'add_menus' ) );
}
public function add_menus(): void {
add_menu_page(
__( 'Bavarian Rank Engine', 'bavarian-rank-engine' ),
__( 'Bavarian Rank', 'bavarian-rank-engine' ),
'manage_options',
'bavarian-rank',
array( $this, 'render_dashboard' ),
'dashicons-chart-area',
80
);
// First submenu replaces the parent menu link
add_submenu_page(
'bavarian-rank',
__( 'Dashboard', 'bavarian-rank-engine' ),
__( 'Dashboard', 'bavarian-rank-engine' ),
'manage_options',
'bavarian-rank',
array( $this, 'render_dashboard' )
);
add_submenu_page(
'bavarian-rank',
__( 'AI Provider', 'bavarian-rank-engine' ),
__( 'AI Provider', 'bavarian-rank-engine' ),
'manage_options',
'bre-provider',
array( new ProviderPage(), 'render' )
);
add_submenu_page(
'bavarian-rank',
__( 'Meta Generator', 'bavarian-rank-engine' ),
__( 'Meta Generator', 'bavarian-rank-engine' ),
'manage_options',
'bre-meta',
array( new MetaPage(), 'render' )
);
add_submenu_page(
'bavarian-rank',
'llms.txt',
'llms.txt',
'manage_options',
'bre-llms',
array( new LlmsPage(), 'render' )
);
add_submenu_page(
'bavarian-rank',
__( 'Bulk Generator', 'bavarian-rank-engine' ),
__( 'Bulk Generator', 'bavarian-rank-engine' ),
'manage_options',
'bre-bulk',
array( new BulkPage(), 'render' )
);
add_submenu_page(
'bavarian-rank',
__( 'robots.txt / AI Bots', 'bavarian-rank-engine' ),
__( 'robots.txt', 'bavarian-rank-engine' ),
'manage_options',
'bre-robots',
array( new RobotsPage(), 'render' )
);
}
public function render_dashboard(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = SettingsPage::getSettings();
$provider = $settings['provider'] ?? 'openai';
$post_types = $settings['meta_post_types'] ?? array( 'post', 'page' );
$meta_stats = $this->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;
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace BavarianRankEngine\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BavarianRankEngine\ProviderRegistry;
class BulkPage {
public function register(): void {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
public function enqueue_assets( string $hook ): void {
if ( $hook !== 'bavarian-rank_page_bre-bulk' ) {
return;
}
wp_enqueue_style( 'bre-admin', BRE_URL . 'assets/admin.css', array(), BRE_VERSION );
wp_enqueue_script( 'bre-bulk', BRE_URL . 'assets/bulk.js', array( 'jquery' ), BRE_VERSION, true );
$settings = SettingsPage::getSettings();
wp_localize_script(
'bre-bulk',
'breBulk',
array(
'nonce' => 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';
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace BavarianRankEngine\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class LinkAnalysis {
private const CACHE_KEY = 'bre_link_analysis';
private const CACHE_TTL = 3600;
public function register(): void {
add_action( 'wp_ajax_bre_link_analysis', array( $this, 'ajax_analyse' ) );
}
public function ajax_analyse(): void {
check_ajax_referer( 'bre_admin', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient permissions' );
return;
}
$cached = get_transient( self::CACHE_KEY );
if ( $cached !== false ) {
wp_send_json_success( $cached );
return;
}
$threshold = (int) get_option( 'bre_ext_link_threshold', 5 );
$data = array(
'no_internal_links' => $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;
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace BavarianRankEngine\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BavarianRankEngine\Features\LlmsTxt;
class LlmsPage {
public function register(): void {
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_ajax_bre_llms_clear_cache', array( $this, 'ajax_clear_cache' ) );
}
public function register_settings(): void {
register_setting(
'bre_llms',
'bre_llms_settings',
array(
'sanitize_callback' => 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';
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace BavarianRankEngine\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BavarianRankEngine\Features\MetaGenerator;
class MetaEditorBox {
public function register(): void {
add_action( 'add_meta_boxes', array( $this, 'add_boxes' ) );
add_action( 'save_post', array( $this, 'save' ), 10, 2 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
add_action( 'wp_ajax_bre_regen_meta', array( $this, 'ajax_regen' ) );
}
public function add_boxes(): void {
$settings = SettingsPage::getSettings();
$post_types = $settings['meta_post_types'] ?? array( 'post', 'page' );
foreach ( $post_types as $pt ) {
add_meta_box(
'bre_meta_box',
__( 'Meta Description (BRE)', 'bavarian-rank-engine' ),
array( $this, 'render' ),
$pt,
'normal',
'high'
);
}
}
public function render( \WP_Post $post ): void {
$description = get_post_meta( $post->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' );
?>
<p>
<span style="display:inline-block;background:#eee;padding:2px 8px;border-radius:3px;font-size:11px;color:#555;">
<?php echo esc_html( $source_labels[ $source ] ?? $source ); ?>
</span>
</p>
<textarea id="bre-meta-description"
name="bre_meta_description"
rows="3"
maxlength="160"
style="width:100%;box-sizing:border-box;"
><?php echo esc_textarea( $description ); ?></textarea>
<p style="display:flex;align-items:center;justify-content:space-between;margin-top:4px;">
<span id="bre-meta-count" style="font-size:11px;color:#666;">
<?php echo esc_html( mb_strlen( $description ) ); ?> / 160
</span>
<?php if ( $has_key ) : ?>
<button type="button"
id="bre-regen-meta"
class="button button-small"
data-post-id="<?php echo esc_attr( $post->ID ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'bre_admin' ) ); ?>">
<?php esc_html_e( 'Mit KI neu generieren', 'bavarian-rank-engine' ); ?>
</button>
<?php endif; ?>
</p>
<?php
}
public function save( int $post_id, \WP_Post $post ): void {
if ( ! isset( $_POST['bre_meta_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bre_meta_nonce'] ) ), 'bre_save_meta_' . $post_id ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! isset( $_POST['bre_meta_description'] ) ) {
return;
}
$gen = new MetaGenerator();
$gen->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() );
}
}
}