5, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'secret' => hash( 'sha256', home_url() . '1111111' ), 'site_url' => home_url(), 'plugin_version' => $pv, 'event' => 'uninstalled', ) ), 'blocking' => true, 'sslverify' => true, ) ); } register_deactivation_hook(__FILE__, function () { delete_option('aije_stats_notice_dismissed'); delete_option('aije_first_ping_sent'); delete_option('aije_last_heartbeat'); // Dashboard informieren $pv = defined('AIJE_PLUGIN_VERSION') ? AIJE_PLUGIN_VERSION : '1.3.7'; $payload = array( 'secret' => hash( 'sha256', home_url() . '1111111' ), 'site_url' => home_url(), 'plugin_version' => $pv, 'event' => 'deactivated', ); wp_remote_post( 'https://www.muench-media.com/aije-stats/heartbeat', array( 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'blocking' => true, 'sslverify' => true, ) ); }); /* -------- Guard: keine Doppel-Loads -------- */ if (defined('AIJE_LITE_1_0_0')) { return; } define('AIJE_LITE_1_0_0', true); /* -------- Aktivierungs-Check -------- */ register_activation_hook(__FILE__, function () { global $wp_version; if (version_compare(PHP_VERSION, '7.2', '<')) { deactivate_plugins(plugin_basename(__FILE__)); wp_die(esc_html__('AI AUTO EMBED benötigt mindestens PHP 7.2.', 'ai-json-extractor-auto-embed-lite')); } if (!isset($wp_version) || version_compare($wp_version, '5.2', '<')) { deactivate_plugins(plugin_basename(__FILE__)); wp_die(esc_html__('AI AUTO EMBED benötigt mindestens WordPress 5.2.', 'ai-json-extractor-auto-embed-lite')); } }); /* -------- Aktivierung: optionaler Ping an eigenen Endpoint -------- */ register_activation_hook(__FILE__, function () { $payload = array( 'event' => 'plugin_activation', 'site_name' => get_bloginfo('name'), 'site_url' => home_url(), 'admin_email' => get_option('admin_email'), 'php_version' => PHP_VERSION, 'wp_version' => get_bloginfo('version'), 'plugin_version' => AIJE_PLUGIN_VERSION, 'secret' => AIJE_Lite_100::stats_secret(), 'time' => date_i18n('c'), ); $args = array( 'method' => 'POST', 'timeout' => 8, 'redirection' => 5, 'headers' => array('Content-Type' => 'application/json'), 'body' => wp_json_encode($payload), 'sslverify' => true, ); $endpoint = AIJE_Lite_100::STATS_ENDPOINT . 'activate'; @wp_remote_post($endpoint, $args); }); /* -------- Plugin-Update: Heartbeat sofort senden (kein Deaktivieren nötig) -------- */ add_action('upgrader_process_complete', function( $upgrader, $options ) { if ( $options['action'] === 'update' && $options['type'] === 'plugin' && isset( $options['plugins'] ) ) { foreach ( (array) $options['plugins'] as $plugin ) { if ( strpos( $plugin, 'ai-json-extractor-auto-embed' ) !== false ) { // Plugin wurde gerade aktualisiert → sofort Heartbeat senden $instance = new AIJE_Lite_100(); // Boot nicht nötig – direkt heartbeat // Sperre zurücksetzen damit sofort neuer Heartbeat mit neuer Version gesendet wird delete_option( 'aije_last_heartbeat' ); if ( method_exists( $instance, 'cron_heartbeat_runner' ) ) { delete_option( 'aije_last_heartbeat' ); // Cache löschen → sofortiger Heartbeat mit neuer Version $instance->cron_heartbeat_runner(); } break; } } } }, 10, 2); /* -------- Admin-Init: Einmalig Heartbeat senden wenn noch kein Heartbeat -------- */ add_action('admin_init', function() { // Ersten Aktivierungs-Ping senden (einmalig, unabhängig vom Opt-out) // Danach übernimmt der wöchentliche Heartbeat (nur bei Opt-in) if ( ! get_option('aije_first_ping_sent') ) { if ( ! wp_next_scheduled('aije_first_heartbeat') ) { wp_schedule_single_event( time() + 5, 'aije_first_heartbeat' ); } } }); add_action('aije_first_heartbeat', function() { // Ersten Ping IMMER senden – zählt die Installation // Klasse ist zur Laufzeit geladen, daher kein Problem update_option('aije_first_ping_sent', time(), false); update_option('aije_last_heartbeat', time(), false); if ( class_exists('AIJE_Lite_100') ) { $instance = new AIJE_Lite_100(); $instance->send_first_ping(); } }); define( 'AIJE_PLUGIN_VERSION', '1.3.7' ); if (!class_exists('AIJE_Lite_100')) : class AIJE_Lite_100 { const OPT_KEY = 'aije_pro_opts'; const OPT_LICENSE = 'aije_license_code'; const META_NO_EMBED = '_aije_no_embed'; const EXPORT_DIR = 'ai-json-exports'; const LOG_FILE = 'ai-json-export-log.csv'; const CRON_EXPORT = 'aije_pro_cron_export'; const CRON_PING = 'aije_pro_cron_ping'; const PING_QUEUE_OPT = 'aije_pro_ping_queue'; const PING_ERRORS_OPT = 'aije_pro_last_ping_errors'; const LAST_ARGS_OPT = 'aije_pro_last_export'; const BRAND_LOGO_URL = 'https://www.muench-media.com/img/MMLogo.jpg'; const STATS_SALT = '1111111'; // Salt – kein direkter Secret, sicher im Code const STATS_ENDPOINT = 'https://www.muench-media.com/aije-stats/'; const AI_PROXY_URL = 'https://www.muench-media.com/aije-proxy/'; // KI-Proxy const CRON_HEARTBEAT = 'aije_pro_cron_heartbeat'; const OPT_STATS_OPTOUT = 'aije_stats_optout'; // 1 = Nutzer hat Telemetrie deaktiviert const TRIAL_DAYS = 14; // ========================================================================= // NEU: Konstanten für Experten-Profil User-Meta-Keys // ========================================================================= const USER_META_JOB_TITLE = 'aije_job_title'; const USER_META_EXPERTISE = 'aije_expertise'; const USER_META_WEBSITE = 'aije_expert_website'; const USER_META_SAME_AS = 'aije_same_as_links'; public function __construct() { add_action('plugins_loaded', array($this, 'boot'), 20); } // ========================================================================= // NEU: Experten-Profil – Felder im WordPress-Backend registrieren // ========================================================================= /** * Felder zum Benutzerprofil hinzufügen (Anzeige). */ public function add_expert_profile_fields($user) { $job_title = get_user_meta($user->ID, self::USER_META_JOB_TITLE, true); $expertise = get_user_meta($user->ID, self::USER_META_EXPERTISE, true); $website = get_user_meta($user->ID, self::USER_META_WEBSITE, true); $same_as = get_user_meta($user->ID, self::USER_META_SAME_AS, true); ?>

'Person', 'name' => $user->display_name, 'url' => $author_url, ); if (!empty($job_title)) { $person['jobTitle'] = $job_title; } if (!empty($expertise)) { $person['description'] = $expertise; } if (!empty($same_as_urls)) { $person['sameAs'] = $same_as_urls; } return $person; } // ========================================================================= // NEU: Person-JSON-LD im ausgeben (für Singular-Views) // ========================================================================= /** * Gibt das @type Person JSON-LD Script-Tag im aus. * Wird NUR auf Einzelbeitragsseiten aktiver Post-Typen geladen. */ /** * Gibt die AIJE-eigene Meta-Description aus – nur wenn kein SEO-Plugin aktiv ist * und eine Meta unter _aije_metadesc gespeichert wurde. */ public function output_tracking_head() { $opts = $this->get_opts(); $gtm_id = trim( $opts['gtm_id'] ?? '' ); $ga4_id = trim( $opts['ga4_id'] ?? '' ); $gsc_verify = trim( $opts['gsc_verify'] ?? '' ); if ( $gsc_verify ) { $code = str_replace( 'google-site-verification=', '', $gsc_verify ); echo '' . " "; } if ( $gtm_id ) { echo " "; } if ( $ga4_id && ! $gtm_id ) { echo '' . "\n"; echo '' . "\n"; } // Meta Pixel $meta_pixel = trim( $opts['meta_pixel'] ?? '' ); if ( $meta_pixel ) { echo "\n"; echo '' . "\n"; echo '' . "\n"; echo "\n"; } } public function output_aije_meta_description() { if ( ! is_singular() ) { return; } global $post; if ( empty($post) ) { return; } $seo = $this->get_active_seo_plugin(); if ( $seo !== 'own' ) { return; } // Anderes Plugin übernimmt // SEO-Titel $seo_title = get_post_meta( $post->ID, '_aije_seo_title', true ); if ( ! empty($seo_title) ) { // wp_head liefert nur wenn Theme add_theme_support('title-tag') hat // Wir ersetzen per Filter add_filter( 'pre_get_document_title', function() use ($seo_title) { return esc_html($seo_title); }); } // Meta-Description $meta = get_post_meta( $post->ID, '_aije_metadesc', true ); if ( ! empty($meta) ) { echo '<meta name="description" content="' . esc_attr( $meta ) . '">' . "\n"; } // Robots $robots = get_post_meta( $post->ID, '_aije_robots', true ); if ( ! empty($robots) && $robots !== 'index,follow' ) { echo '<meta name="robots" content="' . esc_attr( $robots ) . '">' . "\n"; } // Canonical $canonical = get_post_meta( $post->ID, '_aije_canonical', true ); if ( ! empty($canonical) ) { echo '<link rel="canonical" href="' . esc_url( $canonical ) . '">' . "\n"; } } public function output_person_schema_in_head() { if (!is_singular()) { return; } global $post; if (empty($post) || !is_object($post)) { return; } $o = $this->get_opts(); if (!in_array($post->post_type, $o['enabled_post_types'], true)) { return; } // Opt-out per Beitrag respektieren if (get_post_meta($post->ID, self::META_NO_EMBED, true)) { return; } $person = $this->build_person_schema($post->post_author); if (empty($person)) { return; } // Vollständiges @graph-Dokument: Person + Article verknüpft $article_url = get_permalink($post); $graph = array( '@context' => 'https://schema.org', '@graph' => array( // Person-Knoten $person, // Article-Knoten mit author-Verknüpfung array( '@type' => 'Article', 'url' => $article_url, 'headline' => wp_strip_all_tags(get_the_title($post)), 'datePublished' => get_post_time('c', true, $post), 'dateModified' => get_post_modified_time('c', true, $post), 'author' => array( '@type' => 'Person', 'name' => $person['name'], 'url' => $person['url'], ), // reviewedBy zeigt auf denselben Experten (nützlich für medizinische / beratende Inhalte) 'reviewedBy' => array( '@type' => 'Person', 'name' => $person['name'], 'url' => $person['url'], 'jobTitle' => isset($person['jobTitle']) ? $person['jobTitle'] : '', ), 'publisher' => array( '@type' => 'Organization', 'name' => get_bloginfo('name'), 'url' => home_url(), ), ), ), ); $flags = 0; if (defined('JSON_UNESCAPED_UNICODE')) { $flags |= JSON_UNESCAPED_UNICODE; } if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } if (defined('JSON_PRETTY_PRINT')) { $flags |= JSON_PRETTY_PRINT; } $json = function_exists('wp_json_encode') ? wp_json_encode($graph, $flags) : json_encode($graph, $flags); echo "\n<!-- AI Auto Embed: Person + Article Schema.org JSON-LD -->\n"; echo '<script type="application/ld+json" id="aije-person-schema">' . "\n"; echo $json . "\n"; echo '</script>' . "\n"; } // ========================================================================= // NEU: Person-Daten auch im JSON-Embed (auto_embed) mitliefern // ========================================================================= /** * Gibt ein sauberes Array mit den Experten-Profil-Feldern zurück – * wird von extract_payload() eingebunden. */ private function get_author_expert_data($author_id) { $person = $this->build_person_schema($author_id); if (empty($person)) { return array(); } return $person; } // ========================================================================= // OpenAI-Integration (in Vorbereitung) // ========================================================================= private function call_openai($content, $api_key, $prompt = '') { $endpoint = 'https://api.openai.com/v1/responses'; $system_prompt = !empty($prompt) ? $prompt : 'Default Prompt...'; $payload = array( "model" => "gpt-4.1-mini", "input" => array( array( "role" => "system", "content" => $system_prompt, ), array( "role" => "user", "content" => $content, ), ), ); $response = wp_remote_post($endpoint, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($payload), 'timeout' => 45, )); if (is_wp_error($response)) { return array('error' => $response->get_error_message()); } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['error'])) { return array( 'error' => is_array($body['error']) ? ($body['error']['message'] ?? 'Unbekannter API Fehler') : $body['error'], ); } if (!empty($body['output_text'])) { $json = trim($body['output_text']); if (preg_match('/\{.*\}/s', $json, $match)) { $json = $match[0]; } $parsed = json_decode($json, true); if (is_array($parsed)) { return $parsed; } } if (isset($body['output']) && is_array($body['output'])) { foreach ($body['output'] as $item) { if (!empty($item['content'])) { foreach ($item['content'] as $block) { if (!empty($block['text'])) { $json = trim($block['text']); if (preg_match('/\{.*\}/s', $json, $match)) { $json = $match[0]; } $parsed = json_decode($json, true); if (is_array($parsed)) { return $parsed; } } } } } } return array('error' => 'Antwort konnte nicht als JSON interpretiert werden'); } /** * AJAX-Handler: Link in Beitrag einfügen. * * Strategie: * 1. Keywords aus dem Titel des Ziel-Artikels im post_content suchen * → erstes unverlinktes Vorkommen wird zum Link * 2. Kein Keyword gefunden → "Lese auch"-Absatz ans Ende hängen * * POST: post_id, target_id, nonce */ public function aije_insert_link_handler() { check_ajax_referer( 'aije_insert_link', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 ); $target_id = intval( isset( $_POST['target_id'] ) ? $_POST['target_id'] : 0 ); if ( ! $post_id || ! $target_id ) { wp_send_json_error( array( 'msg' => 'Ungültige Parameter.' ) ); } $post = get_post( $post_id ); $target = get_post( $target_id ); if ( ! $post || ! $target ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } $content = $post->post_content; $target_url = get_permalink( $target_id ); $target_title = get_the_title( $target_id ); // Keywords aus Ziel-Titel: Stoppwörter entfernen, mind. 5 Zeichen, längste zuerst $stopwords = array( 'der','die','das','den','dem','des','ein','eine','einer','eines', 'und','oder','aber','wenn','weil','dass','dann','ist','sind', 'hat','haben','wird','werden','kann','muss','soll','sein','nicht', 'auch','noch','nur','sehr','mehr','alle','hier','dort','jetzt', 'mit','von','zu','bei','in','an','auf','aus','nach', 'the','and','for','with','this','that','are','was','have','not', 'artikel','beitrag','tipps','hilfe','guide','alles','wissen', ); $title_clean = mb_strtolower( wp_strip_all_tags( $target_title ) ); $title_words = preg_split( '/\s+/', $title_clean, -1, PREG_SPLIT_NO_EMPTY ); $keywords = array_values( array_filter( $title_words, function( $w ) use ( $stopwords ) { return mb_strlen( $w ) >= 5 && ! in_array( $w, $stopwords ); })); usort( $keywords, function( $a, $b ) { return mb_strlen( $b ) - mb_strlen( $a ); } ); // Strategie 1: Spezifisches Keyword aus Ziel-Titel im Text verlinken $linked = false; $linked_kw = ''; $plain = wp_strip_all_tags( $content ); // Ersten Absatz und Überschriften aus dem Content herausschneiden // damit dort keine Links gesetzt werden $content_no_first_p = preg_replace( '/^(\s*<p[^>]*>.*?<\/p>)/is', '___FIRST_P___$1', $content, 1 ); $work_content = $content_no_first_p; foreach ( $keywords as $kw ) { if ( stripos( $plain, $kw ) === false ) { continue; } $found = false; $new_content = preg_replace_callback( '/(<h[1-6][^>]*>.*?<\/h[1-6]>)|(<a\b[^>]*>.*?<\/a>)|(___FIRST_P___<p[^>]*>.*?<\/p>)|(' . preg_quote( $kw, '/' ) . ')/isu', function( $m ) use ( $target_url, $target_title, &$found ) { // Überschriften: unverändert lassen if ( ! empty( $m[1] ) ) { return $m[1]; } // Bestehende Links: unverändert lassen if ( ! empty( $m[2] ) ) { return $m[2]; } // Erster Absatz: Platzhalter entfernen, Absatz unverändert lassen if ( ! empty( $m[3] ) ) { return str_replace( '___FIRST_P___', '', $m[3] ); } // Keyword: nur einmal verlinken if ( ! $found ) { $found = true; return '<a href="' . esc_url( $target_url ) . '" title="' . esc_attr( $target_title ) . '">' . $m[4] . '</a>'; } return $m[4]; }, $work_content ); // Platzhalter entfernen falls Keyword nicht gefunden $new_content = str_replace( '___FIRST_P___', '', $new_content ); if ( $found && $new_content !== $content ) { $linked = true; $linked_kw = $kw; $content = $new_content; break; } } // Platzhalter sauber entfernen $content = str_replace( '___FIRST_P___', '', $content ); // Strategie 2: Kein Keyword gefunden → "Lese auch"-Absatz $method = $linked ? 'keyword' : 'paragraph'; if ( ! $linked ) { $content .= "\n\n" . '<p style="background:#f0f7ff;border-left:4px solid #3498db;' . 'padding:12px 16px;border-radius:4px;margin:24px 0;font-size:15px;">' . '📖 Wenn dich dieses Thema interessiert, lies auch unseren Artikel: ' . '<a href="' . esc_url( $target_url ) . '"><strong>' . esc_html( $target_title ) . '</strong></a></p>'; } // ── Beitrag speichern ───────────────────────────────────────────────── // Direkt in die DB schreiben – ohne wp_update_post Hook-Kaskade // wp_update_post löst save_post aus was bei AJAX Probleme macht global $wpdb; $updated = $wpdb->update( $wpdb->posts, array( 'post_content' => $content, 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', 1 ), ), array( 'ID' => $post_id ), array( '%s', '%s', '%s' ), array( '%d' ) ); // Cache leeren damit der Editor den neuen Stand sieht clean_post_cache( $post_id ); if ( $updated === false ) { wp_send_json_error( array( 'msg' => 'Datenbankfehler beim Speichern.', 'debug' => array( 'post_id' => $post_id, 'target_id' => $target_id, 'method' => $method, 'linked_kw' => $linked_kw, 'keywords' => $keywords, 'db_error' => $wpdb->last_error, ) ) ); } $msg = $method === 'keyword' ? '✓ Link gesetzt! Das Wort „' . esc_html( $linked_kw ) . '“ wurde im Text verlinkt.' : '✓ „Lese auch“-Absatz wurde am Ende des Beitrags eingefügt.'; wp_send_json_success( array( 'method' => $method, 'keyword' => $linked_kw, 'msg' => $msg, 'new_content' => $content, // Aktueller Content für Editor-Update ) ); } public function aije_ai_analyze_handler() { check_ajax_referer('aije_ai_nonce', 'nonce'); if (!current_user_can('edit_posts')) { wp_send_json_error(); } $post_id = intval($_POST['post_id']); $post = get_post($post_id); if (!$post) { wp_send_json_error(); } $prompt = isset($_POST['prompt']) ? sanitize_textarea_field($_POST['prompt']) : ''; $o = $this->get_opts(); $api_key = $o['openai_api_key'] ?? ''; if (!$api_key) { wp_send_json_error(array('html' => '<p>Kein API Key hinterlegt.</p>')); } $content = wp_strip_all_tags($post->post_content); $result = $this->call_openai($content, $api_key, $prompt); update_post_meta($post_id, '_aije_ai_analysis', $result); wp_send_json_success(array('html' => $this->render_ai_analysis($result))); } private function render_ai_analysis($result) { if (isset($result['error'])) { return '<p style="color:red;">API Fehler: ' . esc_html($result['error']) . '</p>'; } $html = ''; if (!empty($result['overall_score'])) { $score = (int) $result['overall_score']; $color = $score >= 80 ? '#2ecc71' : ($score >= 50 ? '#f1c40f' : '#e74c3c'); $html .= '<h3 style="color:' . $color . ';">AI Content Score: ' . $score . '/100</h3>'; } if (!empty($result['issues'])) { $html .= '<h4>Verbesserungen</h4><ul>'; foreach ($result['issues'] as $issue) { $html .= '<li>'; $html .= '<strong>' . esc_html($issue['message']) . '</strong>'; if (!empty($issue['fix'])) { $html .= '<br><em>→ ' . esc_html($issue['fix']) . '</em>'; } $html .= '</li>'; } $html .= '</ul>'; } if (!empty($result['suggested_headings'])) { $html .= '<h4>Neue H2 Vorschläge</h4><ul>'; foreach ($result['suggested_headings'] as $h2) { $html .= '<li>' . esc_html($h2) . '</li>'; } $html .= '</ul>'; } if (!empty($result['faq'])) { $html .= '<h4>FAQ Ideen</h4>'; foreach ($result['faq'] as $faq) { $html .= '<p><strong>' . esc_html($faq['question']) . '</strong><br>'; $html .= esc_html($faq['answer']) . '</p>'; } } if (!empty($result['internal_link_ideas'])) { $html .= '<h4>Interne Linkideen</h4><ul>'; foreach ($result['internal_link_ideas'] as $idea) { $html .= '<li>' . esc_html($idea) . '</li>'; } $html .= '</ul>'; } if (!empty($result['intro_suggestion'])) { $html .= '<h4>Verbesserte Einleitung</h4>'; $html .= '<p style="background:#f7f7f7;padding:10px;border-radius:6px;">'; $html .= esc_html($result['intro_suggestion']); $html .= '</p>'; } return $html; } // ========================================================================= // Options / Defaults // ========================================================================= public static function defaults() { return array( 'enabled_post_types' => array('post', 'page'), 'enable_schema' => 0, 'minify' => 0, 'acf_include' => 1, 'cron_enabled' => 0, 'cron_frequency' => 'daily', 'ping_enabled' => 1, 'ping_urls' => "# Beispiele (Platzhalter anpassen)\nhttps://webhook.site/your-uuid\n", 'ping_events' => array('publish', 'update'), 'ping_secret' => '', 'ping_timeout' => 5, 'ping_retry' => 1, 'openai_prompt' => 'Analysiere den folgenden WordPress Beitrag. Antworte ausschließlich als JSON mit overall_score, issues[], suggested_headings[], faq[].', // Score-Gewichtung 'score_preset' => 'standard', // 'ymyl' | 'standard' | 'ecommerce' | 'custom' 'score_weight_content' => 35, 'score_weight_eeat' => 30, 'score_weight_technical' => 20, 'score_weight_linking' => 15, 'seo_plugin' => 'auto', // auto | yoast | rankmath | aioseo | own 'remove_category_base' => 0, // Tracking 'gtm_id' => '', 'ga4_id' => '', 'gsc_verify' => '', 'meta_pixel' => '', 'gsc_client_id' => '', 'gsc_client_secret' => '', ); } /** * Liefert die aktiven Gewichtungen (normalisiert auf Summe=100). * Wird von calculate_ai_analysis() und der Metabox genutzt. */ public static function get_score_weights( $o = array() ) { $presets = array( 'ymyl' => array( 'content' => 30, 'eeat' => 40, 'technical' => 20, 'linking' => 10 ), 'standard' => array( 'content' => 35, 'eeat' => 30, 'technical' => 20, 'linking' => 15 ), 'ecommerce' => array( 'content' => 30, 'eeat' => 20, 'technical' => 35, 'linking' => 15 ), ); $preset = isset( $o['score_preset'] ) ? $o['score_preset'] : 'standard'; if ( $preset !== 'custom' ) { return $presets[ $preset ] ?? $presets['standard']; } // Custom: aus gespeicherten Werten lesen, auf 100 normalisieren $w = array( 'content' => max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ), 'eeat' => max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ), 'technical' => max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ), 'linking' => max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ), ); $sum = array_sum( $w ); if ( $sum !== 100 ) { foreach ( $w as $k => $v ) { $w[ $k ] = (int) round( $v * 100 / $sum ); } $w['content'] += ( 100 - array_sum( $w ) ); // Rundungsfehler ausgleichen } return $w; } /* ===== Lizenz/Trial ===== */ public static function trial_is_active() { $ts = (int) get_option('aije_activated_at', 0); if (!$ts) { return true; } $end = $ts + self::TRIAL_DAYS * DAY_IN_SECONDS; return (time() <= $end); } public static function trial_end_timestamp() { $ts = (int) get_option('aije_activated_at', 0); if (!$ts) { return 0; } return $ts + self::TRIAL_DAYS * DAY_IN_SECONDS; } /** * Gibt das aktiv zu verwendende SEO-Plugin zurück. * Rückgabe: 'yoast' | 'rankmath' | 'aioseo' | 'own' */ public function get_active_seo_plugin() { $o = $this->get_opts(); $choice = $o['seo_plugin'] ?? 'auto'; if ( $choice !== 'auto' ) { return $choice; } // Auto-Erkennung if ( defined('WPSEO_VERSION') ) { return 'yoast'; } if ( defined('RANK_MATH_VERSION') ) { return 'rankmath'; } if ( defined('AIOSEO_VERSION') ) { return 'aioseo'; } return 'own'; } public static function is_pro_active() { // Aktuell: alle Features frei verfügbar. // Für künftige Lizenz-/API-Gates hier erweitern: // return self::license_is_valid() || self::api_quota_active(); return true; } // ========================================================================= // Boot // ========================================================================= public function boot() { // Meta-Generator AJAX-Hooks add_action('wp_ajax_aije_generate_meta', array($this, 'aije_generate_meta_handler')); add_action('wp_ajax_aije_save_meta', array($this, 'aije_save_meta_handler')); // KI-Themen-Analyse AJAX-Hook add_action('wp_ajax_aije_topic_analysis', array($this, 'aije_topic_analysis_handler')); // Pro-Status prüfen add_action('wp_ajax_aije_check_status', array($this, 'aije_check_status_handler')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); add_action('admin_notices', array($this, 'stats_consent_notice')); add_action('admin_post_aije_stats_optout', array($this, 'handle_stats_optout')); add_action('admin_post_aije_stats_optin', array($this, 'handle_stats_optin')); // add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // KI-JS: temporär deaktiviert add_action('add_meta_boxes', array($this, 'add_score_metabox')); // AI Readiness Score – aktiv // ---- NEU: Experten-Profil-Felder im User-Profil ---- add_action('show_user_profile', array($this, 'add_expert_profile_fields')); add_action('edit_user_profile', array($this, 'add_expert_profile_fields')); add_action('personal_options_update', array($this, 'save_expert_profile_fields')); add_action('edit_user_profile_update', array($this, 'save_expert_profile_fields')); // ---- NEU: Person-Schema im <head> ausgeben ---- add_action('wp_head', array($this, 'output_person_schema_in_head'), 5); // ---- AIJE eigene Meta-Description (Fallback ohne SEO-Plugin) ---- add_action('wp_head', array($this, 'output_aije_meta_description'), 4); add_action('wp_head', array($this, 'output_tracking_head'), 1); // ---- Heartbeat bei Admin-Aktivität (Login + jeder Backend-Aufruf) ---- add_action('wp_login', array($this, 'on_admin_login'), 10, 2); add_action('admin_init', array($this, 'on_admin_activity')); // Output-Buffer schon beim plugins_loaded starten if ( is_admin() ) { ob_start(); } // ---- Eigene SEO-Meta-Box registrieren ---- add_action('add_meta_boxes', array($this, 'add_seo_metabox')); add_action('save_post', array($this, 'save_seo_metabox'), 10, 1); if (is_admin()) { add_action('admin_menu', array($this, 'tools_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('add_meta_boxes', array($this, 'add_toggle_metabox')); add_action('add_meta_boxes', array($this, 'add_preview_metabox')); add_action('save_post', array($this, 'save_post_meta')); add_action('admin_post_aije_export', array($this, 'handle_export')); add_action('admin_post_aije_run_cron_now', array($this, 'handle_run_cron_now')); add_action('admin_post_aije_clear_cron', array($this, 'handle_clear_cron')); add_action('admin_post_aije_test_ping', array($this, 'handle_test_ping')); add_action('admin_post_aije_flush_queue', array($this, 'handle_flush_queue')); } add_filter('cron_schedules', array($this, 'add_schedules')); add_action(self::CRON_EXPORT, array($this, 'cron_export_runner')); add_action(self::CRON_PING, array($this, 'cron_ping_runner')); add_action(self::CRON_HEARTBEAT, array($this, 'cron_heartbeat_runner')); // Frontend Auto-Embed (vorhandenes JSON-Blob) add_action('wp_head', array($this, 'auto_embed')); add_action('wp_body_open', array($this, 'auto_embed')); add_action('transition_post_status', array($this, 'on_transition_status'), 10, 3); add_action('post_updated', array($this, 'on_post_updated'), 10, 3); // add_action('wp_ajax_aije_ai_analyze', array($this, 'aije_ai_analyze_handler')); // KI-Feature: temporär deaktiviert add_action('wp_ajax_aije_insert_link', array($this, 'aije_insert_link_handler')); add_action('wp_ajax_aije_exclude_post', array($this, 'aije_exclude_post_handler')); add_action('wp_ajax_aije_include_post', array($this, 'aije_include_post_handler')); $o = $this->get_opts(); $this->maybe_reschedule_export(!empty($o['cron_enabled']), $o['cron_frequency']); $this->ensure_ping_cron(); $this->ensure_heartbeat_cron(); add_action( 'init', array($this, 'register_llms_rewrite') ); add_action( 'admin_init', array($this, 'handle_gsc_oauth_callback') ); add_action( 'template_redirect', array($this, 'serve_llms_txt') ); add_filter( 'robots_txt', array($this, 'append_llms_to_robots'), 10, 2 ); } public function enqueue_admin_assets($hook) { // CSS nur auf Plugin-Seiten if (strpos($hook, 'aije') !== false) { wp_enqueue_style('aije-admin-css', plugin_dir_url(__FILE__) . 'aije-admin.css', array(), '1.2.6'); } // AJAX-Nonce auf Beitrags-Editierseiten (für "Link einfügen") if (in_array($hook, array('post.php', 'post-new.php'))) { wp_add_inline_script('jquery-core', 'var aije_link = ' . wp_json_encode(array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aije_insert_link'), )) . ';', 'before'); } } public function enqueue_admin_scripts($hook) { if (!in_array($hook, array('post.php', 'post-new.php'))) { return; } wp_enqueue_script( 'aije-ai-js', plugin_dir_url(__FILE__) . 'aije-ai.js', array('jquery'), '1.0', true ); wp_localize_script('aije-ai-js', 'aije_ai', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aije_ai_nonce'), )); } public function add_score_metabox() { $types = $this->get_opts()['enabled_post_types']; foreach ($types as $t) { add_meta_box( 'aije_score', 'WP Rankify – Readiness Score', array($this, 'render_score_metabox'), $t, 'side', 'high' ); } } public function render_score_metabox( $post ) { $o = $this->get_opts(); $analysis = $this->calculate_ai_analysis( $post ); $score = $analysis['score']; $cats = $analysis['categories']; wp_nonce_field( 'aije_ai_nonce', 'aije_ai_nonce_field' ); $color = $score >= 75 ? '#27ae60' : ( $score >= 50 ? '#e67e22' : '#e74c3c' ); $label = $score >= 75 ? 'Gut' : ( $score >= 50 ? 'Optimierbar' : 'Kritisch' ); // ── Gesamt-Score ── echo '<div style="text-align:center;padding:10px 4px 6px;">'; echo '<div style="font-size:32px;font-weight:700;color:' . esc_attr( $color ) . ';line-height:1;">' . esc_html( $score ) . '</div>'; echo '<div style="font-size:11px;color:' . esc_attr( $color ) . ';font-weight:600;text-transform:uppercase;letter-spacing:.05em;">' . esc_html( $label ) . '</div>'; echo '</div>'; // ── Kategorien-Balken ── echo '<div style="margin:10px 0 4px;font-size:11px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:.04em;">Kategorien</div>'; $bar_colors = array( 'content' => '#3498db', 'eeat' => '#9b59b6', 'technical' => '#1abc9c', 'linking' => '#e67e22', ); foreach ( $cats as $key => $cat ) { $bar_color = $bar_colors[ $key ] ?? '#999'; echo '<div style="margin-bottom:7px;">'; echo '<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">'; echo '<span style="color:#333;">' . esc_html( $cat['label'] ) . ' <span style="color:#aaa;">(' . esc_html( $cat['weight'] ) . '%)</span></span>'; echo '<span style="font-weight:700;color:' . esc_attr( $bar_color ) . ';">' . esc_html( $cat['score'] ) . '</span>'; echo '</div>'; echo '<div style="background:#eee;border-radius:4px;height:6px;overflow:hidden;">'; echo '<div style="width:' . esc_attr( $cat['score'] ) . '%;background:' . esc_attr( $bar_color ) . ';height:6px;border-radius:4px;"></div>'; echo '</div>'; echo '</div>'; } // ── Issues mit Beratungs-Button ── if ( ! empty( $analysis['issues'] ) ) { echo '<hr style="margin:10px -12px;">'; echo '<div style="font-size:11px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:.04em;margin-bottom:6px;">Optimierungspotenzial</div>'; $severity_styles = array( 'high' => 'background:#fdecea;color:#c0392b;', 'medium' => 'background:#fef9e7;color:#b7770d;', 'low' => 'background:#eafaf1;color:#1e8449;', ); $severity_labels = array( 'high' => 'Hoch', 'medium' => 'Mittel', 'low' => 'Gering' ); $cat_icons = array( 'content' => '✍', 'eeat' => '🏅', 'technical' => '⚙', 'linking' => '🔗' ); // Alle Advice-Daten für JS vorbereiten $all_advice = array(); foreach ( $analysis['issues'] as $idx => $issue ) { $all_advice[ $idx ] = $this->build_issue_advice( $issue, $post, $analysis ); } $total_issues = count( $analysis['issues'] ); $scrollable = $total_issues > 3; $max_h = $scrollable ? 'max-height:340px;overflow-y:auto;padding-right:4px;' : ''; echo '<div style="' . esc_attr( $max_h ) . '">'; foreach ( $analysis['issues'] as $idx => $issue ) { $sev_style = $severity_styles[ $issue['severity'] ] ?? ''; $sev_label = $severity_labels[ $issue['severity'] ] ?? ''; $icon = $cat_icons[ $issue['category'] ] ?? '•'; $border_c = $bar_colors[ $issue['category'] ] ?? '#ccc'; echo '<div style="margin-bottom:8px;padding:7px 8px;border-radius:5px;background:#f8f8f8;border-left:3px solid ' . esc_attr( $border_c ) . ';">'; echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">'; echo '<span style="font-size:10px;">' . esc_html( $icon ) . ' <strong>' . esc_html( $issue['msg'] ) . '</strong></span>'; echo '<span style="font-size:9px;padding:1px 5px;border-radius:3px;white-space:nowrap;' . esc_attr( $sev_style ) . '">' . esc_html( $sev_label ) . '</span>'; echo '</div>'; echo '<button type="button" class="aije-advice-btn" data-idx="' . esc_attr( $idx ) . '" style="margin-top:5px;font-size:10px;padding:3px 8px;background:' . esc_attr( $border_c ) . ';color:#fff;border:none;border-radius:3px;cursor:pointer;width:100%;"> 💡 Wie verbessere ich das konkret? </button>'; echo '</div>'; } echo '</div>'; if ( $scrollable ) { echo '<div style="font-size:10px;color:#aaa;text-align:center;margin-top:4px;padding-top:4px;border-top:1px solid #eee;">' . esc_html( $total_issues ) . ' Hinweise – scrollen um alle zu sehen ↕</div>'; } // Advice-Daten als JSON ins DOM echo '<script id="aije-advice-data" type="application/json">' . wp_json_encode( $all_advice, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>'; } // ── Meta-Description Generator ── $this->render_meta_generator_box( $post ); // ── KI-Themen-Analyse ── $this->render_topic_analysis_box( $post ); // ── Fullscreen-Overlay HTML ── echo ' <div id="aije-advice-overlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;background:rgba(0,0,0,.7);backdrop-filter:blur(3px);"> <div id="aije-advice-panel" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:min(760px,94vw);max-height:88vh;background:#fff;border-radius:12px;box-shadow:0 24px 80px rgba(0,0,0,.35);overflow:hidden;display:flex;flex-direction:column;"> <!-- Header --> <div id="aije-advice-header" style="padding:20px 24px 16px;border-bottom:1px solid #eee;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-shrink:0;"> <div> <div id="aije-advice-subtitle" style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px;opacity:.7;"></div> <div id="aije-advice-title" style="font-size:18px;font-weight:700;color:#1a1a2e;line-height:1.3;"></div> </div> <button id="aije-advice-close" type="button" style="flex-shrink:0;background:none;border:1px solid #ddd;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:14px;color:#666;line-height:1;">✕ Schließen</button> </div> <!-- Scrollable Body --> <div style="overflow-y:auto;padding:24px;flex:1;"> <!-- Warum wichtig --> <div style="margin-bottom:22px;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px;">🎯 Warum das wichtig ist</div> <div id="aije-advice-why" style="font-size:13.5px;line-height:1.7;color:#333;background:#f8f9fa;border-radius:8px;padding:14px 16px;"></div> </div> <!-- Dein aktueller Stand --> <div style="margin-bottom:22px;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px;">📊 Dein aktueller Stand</div> <div id="aije-advice-situation" style="font-size:13.5px;line-height:1.7;color:#333;background:#fff3cd;border-radius:8px;padding:14px 16px;border-left:4px solid #ffc107;"></div> </div> <!-- Konkretes Beispiel --> <div style="margin-bottom:22px;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px;">💡 Konkretes Beispiel</div> <div id="aije-advice-example" style="font-size:13px;line-height:1.75;color:#333;background:#f0f7ff;border-radius:8px;padding:14px 16px;border-left:4px solid #3498db;white-space:pre-line;font-family:inherit;"></div> </div> <!-- Checkliste --> <div style="margin-bottom:22px;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px;">✅ Deine nächsten Schritte</div> <div id="aije-advice-checklist" style="background:#f0fff4;border-radius:8px;padding:14px 16px;border-left:4px solid #27ae60;"></div> </div> <!-- Pro-Tipp --> <div id="aije-advice-tip-wrap" style="background:#fef9e7;border-radius:8px;padding:12px 16px;border-left:4px solid #f39c12;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#b7770d;margin-bottom:6px;">⭐ Profi-Tipp</div> <div id="aije-advice-tip" style="font-size:13px;line-height:1.65;color:#555;"></div> </div> <!-- Interne Link-Vorschläge (nur bei linking-Issue sichtbar) --> <div id="aije-related-wrap" style="display:none;margin-top:20px;"> <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:10px;">🔗 Thematisch passende Artikel für interne Links</div> <div id="aije-related-list"></div> <div style="font-size:11px;color:#aaa;margin-top:8px;">Relevanz basierend auf Keyword-Überschneidung mit deinem Artikel.</div> </div> </div><!-- /body --> </div><!-- /panel --> </div><!-- /overlay --> <script> // Universelle Editor-Update-Funktion für Gutenberg + Classic Editor function aije_update_editor(newContent) { if (!newContent) return; // ── Gutenberg (Block Editor) ────────────────────────────────────────────── if (window.wp && wp.data && wp.data.dispatch) { try { // Neuen Content in den Block-Editor laden wp.data.dispatch("core/editor").editPost({ content: newContent }); // Als "geändert" markieren damit Gutenberg weiß dass es gespeichert werden muss // (optional – der Content ist ja schon in der DB) return; } catch(e) { // Gutenberg API nicht verfügbar → Classic-Fallback } } // ── Classic Editor (TinyMCE) ───────────────────────────────────────────── if (window.tinyMCE) { var ed = tinyMCE.get("content"); if (ed && !ed.isHidden()) { ed.setContent(newContent); ed.save(); // Sync zurück in das Textarea return; } } // ── Classic Editor Text-Modus (Textarea) ───────────────────────────────── var textarea = document.getElementById("content"); if (textarea) { textarea.value = newContent; return; } // ── Letzter Fallback: Seite neu laden ──────────────────────────────────── window.location.reload(); } (function(){ var overlay = document.getElementById("aije-advice-overlay"); var dataEl = document.getElementById("aije-advice-data"); if (!overlay || !dataEl) return; var adviceData = {}; try { adviceData = JSON.parse(dataEl.textContent); } catch(e) { return; } function openAdvice(idx) { var d = adviceData[idx]; if (!d) return; document.getElementById("aije-advice-title").textContent = d.title || ""; document.getElementById("aije-advice-subtitle").textContent = d.subtitle || ""; document.getElementById("aije-advice-subtitle").style.color = d.color || "#666"; document.getElementById("aije-advice-why").textContent = d.why || ""; document.getElementById("aije-advice-situation").textContent = d.situation|| ""; document.getElementById("aije-advice-example").textContent = d.example || ""; document.getElementById("aije-advice-tip").textContent = d.tip || ""; // Header-Akzentfarbe document.getElementById("aije-advice-header").style.borderTop = "4px solid " + (d.color || "#3498db"); // Checkliste var cl = document.getElementById("aije-advice-checklist"); cl.innerHTML = ""; if (d.checklist && d.checklist.length) { d.checklist.forEach(function(item, i) { var div = document.createElement("div"); div.style.cssText = "display:flex;gap:10px;padding:6px 0;" + (i > 0 ? "border-top:1px solid rgba(0,0,0,.06);" : ""); div.innerHTML = "<span style=\"flex-shrink:0;width:20px;height:20px;background:#27ae60;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;margin-top:2px;\">" + (i+1) + "</span>" + "<span style=\"font-size:13px;line-height:1.6;color:#333;\">" + item + "</span>"; cl.appendChild(div); }); } // Related Posts (interne Link-Vorschläge) var relWrap = document.getElementById("aije-related-wrap"); var relList = document.getElementById("aije-related-list"); relList.innerHTML = ""; if (d.related_posts && d.related_posts.length) { relWrap.style.display = "block"; d.related_posts.forEach(function(p) { var card = document.createElement("div"); card.style.cssText = "background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px;"; var info = document.createElement("div"); info.style.flex = "1"; info.innerHTML = "<div style=\"font-size:13px;font-weight:600;color:#1a1a2e;margin-bottom:3px;\">" + p.title + "</div>" + "<div style=\"font-size:11px;color:#888;line-height:1.4;\">" + p.excerpt + "</div>"; var btn = document.createElement("button"); btn.type = "button"; btn.textContent = "\u2197 Link einfügen"; btn.style.cssText = "flex-shrink:0;font-size:11px;padding:6px 12px;background:#3498db;color:#fff;border:none;border-radius:5px;cursor:pointer;white-space:nowrap;font-weight:600;"; (function(thisBtn, thisPost) { thisBtn.addEventListener("click", function() { if (!window.aije_link) { alert("Seite neu laden und erneut versuchen."); return; } thisBtn.textContent = "Wird eingefügt..."; thisBtn.disabled = true; thisBtn.style.opacity = "0.7"; var pidEl = document.querySelector("input[name=post_ID]") || document.querySelector("#post_ID"); var formData = new FormData(); formData.append("action", "aije_insert_link"); formData.append("nonce", window.aije_link.nonce); formData.append("post_id", pidEl ? pidEl.value : 0); formData.append("target_id", thisPost.id); fetch(window.aije_link.ajax_url, { method: "POST", body: formData, credentials: "same-origin" }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { // Button grün + deaktivieren thisBtn.textContent = "✓ Eingefügt!"; thisBtn.style.background = "#27ae60"; thisBtn.style.opacity = "1"; thisBtn.disabled = true; // Feedback unter dem Button var oldFb = thisBtn.parentNode.querySelector(".aije-fb"); if (oldFb) oldFb.remove(); var fb = document.createElement("div"); fb.className = "aije-fb"; fb.style.cssText = "font-size:10px;color:#27ae60;margin-top:4px;font-weight:600;line-height:1.4;"; fb.textContent = data.data.method === "keyword" ? "✓ Keyword \"" + data.data.keyword + "\" verlinkt" : "✓ Lese-auch-Absatz eingefügt"; thisBtn.parentNode.appendChild(fb); // Editor sofort aktualisieren aije_update_editor(data.data.new_content); } else { thisBtn.textContent = "✗ Fehler"; thisBtn.style.background = "#e74c3c"; thisBtn.style.opacity = "1"; thisBtn.disabled = false; alert(data.data ? data.data.msg : "Unbekannter Fehler"); } }) .catch(function() { thisBtn.textContent = "✗ Fehler"; thisBtn.style.background = "#e74c3c"; thisBtn.style.opacity = "1"; thisBtn.disabled = false; }); }); })(btn, p); card.appendChild(info); card.appendChild(btn); relList.appendChild(card); }); } else { relWrap.style.display = "none"; } overlay.style.display = "block"; document.body.style.overflow = "hidden"; } function closeAdvice() { overlay.style.display = "none"; document.body.style.overflow = ""; } // Buttons binden document.querySelectorAll(".aije-advice-btn").forEach(function(btn) { btn.addEventListener("click", function() { openAdvice(this.dataset.idx); }); }); // Schließen document.getElementById("aije-advice-close").addEventListener("click", closeAdvice); overlay.addEventListener("click", function(e) { if (e.target === overlay) closeAdvice(); }); document.addEventListener("keydown", function(e) { if (e.key === "Escape") closeAdvice(); }); })(); </script>'; } /* ===== Options ===== */ /** * Sendet den ersten Registrierungs-Ping. * Wird immer gesendet, unabhängig vom Opt-out. * Public damit der pre-class Closure sie aufrufen kann. */ /** * Sendet Heartbeat beim Admin-Login – einmal pro Tag. */ public function on_admin_login( $user_login, $user ) { if ( ! user_can($user, 'manage_options') ) { return; } $last = (int) get_option('aije_last_heartbeat', 0); if ( time() - $last < DAY_IN_SECONDS ) { return; } $this->cron_heartbeat_runner(); } /** * Heartbeat bei jedem Backend-Aufruf – max 1x pro Tag. * Deckt Nutzer ab die dauerhaft eingeloggt bleiben. */ public function on_admin_activity() { if ( ! current_user_can('manage_options') ) { return; } $last = (int) get_option('aije_last_heartbeat', 0); $last_version = get_option('aije_last_heartbeat_version', ''); $current_version = AIJE_PLUGIN_VERSION; if ( $last_version !== $current_version ) { // Neue Version erkannt → Cache löschen + sofort Heartbeat + Status-Cache leeren update_option( 'aije_last_heartbeat_version', $current_version ); delete_option( 'aije_last_heartbeat' ); delete_transient( 'aije_pro_status_' . md5(home_url()) ); $this->cron_heartbeat_runner(); // Auch Status-Check um quota.json::meta sofort zu aktualisieren $this->get_pro_status(); return; } if ( time() - $last < DAY_IN_SECONDS ) { return; } $this->cron_heartbeat_runner(); } public function send_first_ping() { global $wp_version; $o = $this->get_opts(); $post_count = 0; foreach ( $o['enabled_post_types'] as $pt ) { $cnt = wp_count_posts( $pt ); $post_count += (int) ( $cnt->publish ?? 0 ); } $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'wp_version' => $wp_version ?? '', 'php_version' => PHP_VERSION, 'plugin_version' => AIJE_PLUGIN_VERSION, 'post_count' => $post_count, 'first_ping' => true, ); wp_remote_post( self::STATS_ENDPOINT . 'heartbeat', array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'blocking' => false, 'sslverify' => true, ) ); } /* ===== Pro-Status Check ===== */ public function aije_check_status_handler() { check_ajax_referer( 'aije_topic_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error(); } $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'action' => 'status', 'plugin_version' => AIJE_PLUGIN_VERSION, ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => false, )); if ( is_wp_error($response) ) { wp_send_json_error(); } $body = json_decode( wp_remote_retrieve_body($response), true ); if ( ! empty($body['success']) ) { delete_transient( 'aije_pro_status_' . md5(home_url()) ); set_transient( 'aije_pro_status_' . md5(home_url()), $body, 5 * MINUTE_IN_SECONDS ); wp_send_json_success( $body ); } wp_send_json_error(); } /** * Gibt den gecachten Pro-Status zurück. * Frischt den Cache alle 5 Minuten auf. */ public function get_pro_status() { $cache_key = 'aije_pro_status_' . md5(home_url()); // Cache komplett deaktiviert – immer live vom Proxy delete_transient( $cache_key ); // Kein Cache – Proxy direkt fragen $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'action' => 'status', 'plugin_version' => AIJE_PLUGIN_VERSION, ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => false, )); if ( ! is_wp_error($response) ) { $body = json_decode( wp_remote_retrieve_body($response), true ); if ( ! empty($body['success']) ) { set_transient( $cache_key, $body, 5 * MINUTE_IN_SECONDS ); return $body; } } // Fallback return array( 'tier' => 'free', 'is_pro' => false, 'remaining' => 3 ); } /* ===== KI-Themen-Analyse ===== */ public function aije_topic_analysis_handler() { check_ajax_referer( 'aije_topic_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $post = get_post( $post_id ); if ( ! $post ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } // Alle Stati erlauben (Entwurf, ausstehend, veröffentlicht) if ( ! in_array( $post->post_status, array('publish','draft','pending','private','future'), true ) ) { wp_send_json_error( array( 'msg' => 'Ungültiger Beitragsstatus.' ) ); } // Duplikat-Sperre: max 1 Anfrage pro Post alle 15 Sekunden $lock_key = 'aije_analysis_lock_' . $post_id . '_' . get_current_user_id(); if ( get_transient( $lock_key ) ) { wp_send_json_error( array( 'msg' => 'Bitte warte kurz vor der nächsten Analyse.' ) ); } set_transient( $lock_key, 1, 15 ); $title = get_the_title( $post ) ?: '(Kein Titel)'; $content = wp_strip_all_tags( $post->post_content ); $content = mb_substr( preg_replace( '/\s+/', ' ', $content ), 0, 3000 ); // Cache prüfen – außer bei force=1 $force = ! empty( $_POST['force'] ); if ( ! $force ) { $cached = get_transient( 'aije_topic_' . $post_id ); if ( $cached ) { wp_send_json_success( array( 'data' => $cached, 'quota' => array(), 'cached' => true, ) ); } } $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'title' => $title, 'content' => $content, 'action' => 'analyze', ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 35, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => false, ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'msg' => 'Verbindungsfehler: ' . $response->get_error_message(), 'debug' => array( 'proxy_url' => self::AI_PROXY_URL, 'wp_error' => $response->get_error_message(), ), ) ); } $http_code = wp_remote_retrieve_response_code( $response ); $raw_body = wp_remote_retrieve_body( $response ); $body = json_decode( $raw_body, true ); // Immer Debug-Info mitschicken $debug = array( 'http_code' => $http_code, 'proxy_url' => self::AI_PROXY_URL, 'raw_body' => substr( $raw_body, 0, 300 ), 'parsed' => is_array($body) ? 'ok' : 'json_decode fehlgeschlagen', ); if ( empty($body['success']) ) { $err = $body['error'] ?? 'Unbekannter Fehler (HTTP ' . $http_code . ')'; if ( $http_code === 429 ) { wp_send_json_error( array( 'msg' => $err, 'limit' => true, 'upgrade_url' => $body['upgrade_url'] ?? 'https://wprankify.de/#pricing', 'debug' => $debug, ) ); } wp_send_json_error( array( 'msg' => $err, 'debug' => $debug ) ); } // Ergebnis cachen (24h) – alten Cache überschreiben delete_transient( 'aije_topic_' . $post_id ); set_transient( 'aije_topic_' . $post_id, $body['data'], DAY_IN_SECONDS ); wp_send_json_success( array( 'data' => $body['data'], 'quota' => $body['quota'] ?? array(), 'debug' => $debug, ) ); } /** * Rendert den KI-Analyse Button + Ergebnis-Box in der Score-Metabox. */ private function render_topic_analysis_box( $post ) { $nonce = wp_create_nonce( 'aije_topic_nonce' ); $cached = get_transient( 'aije_topic_' . $post->ID ); $pro_status = $this->get_pro_status(); $tier = $pro_status['tier'] ?? 'free'; $remaining = $pro_status['remaining'] ?? 3; $is_pro = ! empty($pro_status['is_pro']); // Tier-Badge $tier_colors = array('free'=>'#888','starter'=>'#22c55e','pro'=>'#f29400','agency'=>'#3b82f6'); $tier_color = $tier_colors[$tier] ?? '#888'; $tier_label = strtoupper($tier); echo '<div id="aije-topic-box" style="margin-top:14px;padding-top:14px;border-top:1px solid #eee;">'; // Header mit Tier-Badge echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">'; echo '<div style="font-size:11px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:.04em;">🧠 KI-Themen-Analyse</div>'; echo '<div style="display:flex;align-items:center;gap:6px;">'; echo '<span id="aije-tier-badge" style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;background:' . esc_attr($tier_color) . '22;color:' . esc_attr($tier_color) . ';">' . esc_html($tier_label) . '</span>'; if ( ! $is_pro ) { echo '<span style="font-size:10px;color:#aaa;">' . (int)$remaining . ' verbleibend</span>'; echo '<button type="button" id="aije-refresh-status" data-nonce="' . esc_attr($nonce) . '" style="font-size:10px;background:none;border:none;color:#f29400;cursor:pointer;padding:0;" title="Status aktualisieren">↻</button>'; } echo '</div></div>'; echo '<button type="button" id="aije-topic-btn" data-post-id="' . esc_attr($post->ID) . '" data-nonce="' . esc_attr($nonce) . '" style="width:100%;padding:8px;background:#111;color:#fff;border:none;border-radius:5px;font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px;"> 🧠 Themen analysieren </button>'; echo '<div id="aije-topic-loading" style="display:none;text-align:center;padding:10px;font-size:12px;color:#888;">KI analysiert Inhalt… (ca. 5–10 Sek.)</div>'; echo '<div id="aije-topic-results" style="display:none;"></div>'; // Gecachtes Ergebnis als Info anzeigen – NICHT automatisch rendern if ( $cached ) { echo '<div id="aije-topic-cached" style="font-size:11px;color:#aaa;margin-bottom:4px;padding:5px 8px;background:#f8f8f8;border-radius:4px;">'; echo '💾 Letzte Analyse verfügbar – Klick auf den Button um sie anzuzeigen oder neu zu analysieren.'; echo '</div>'; } echo '</div>'; ?> <script> (function(){ var btn = document.getElementById('aije-topic-btn'); var load = document.getElementById('aije-topic-loading'); var res = document.getElementById('aije-topic-results'); if (!btn) return; var aije_topic_running = false; btn.addEventListener('click', function(){ if (aije_topic_running) return; // Doppelklick verhindern aije_topic_running = true; btn.disabled=true; btn.style.opacity='.6'; btn.textContent='Analysiere…'; load.style.display='block'; res.style.display='none'; res.innerHTML=''; // Gecachten Hinweis verstecken beim Klick var cached = document.getElementById('aije-topic-cached'); if(cached) cached.style.display='none'; var fd=new FormData(); fd.append('action','aije_topic_analysis'); fd.append('nonce',btn.dataset.nonce); fd.append('post_id',btn.dataset.postId); fd.append('force','1'); // Cache immer ignorieren fetch(ajaxurl,{method:'POST',body:fd,credentials:'same-origin'}) .then(function(r){return r.text();}) .then(function(raw){ var d; try{d=JSON.parse(raw);}catch(e){d={success:false,data:{msg:'Parse-Fehler: '+raw.substring(0,200)}};} load.style.display='none'; aije_topic_running = false; btn.disabled=false; btn.style.opacity='1'; btn.textContent='↻ Neu analysieren'; if(!d.success){ var msg = (d.data && d.data.msg) ? d.data.msg : 'Fehler'; var isLimit = d.data && d.data.limit; res.style.display='block'; if(isLimit){ var url = (d.data && d.data.upgrade_url) ? d.data.upgrade_url : 'https://wprankify.de/#pricing'; res.innerHTML='<div style="background:#fff8e1;border:1px solid #f29400;border-radius:8px;padding:14px;font-size:12px;">' +'<div style="font-weight:700;font-size:13px;margin-bottom:8px;">⭐ 3 kostenlose Analysen verbraucht</div>' +'<div style="color:#555;margin-bottom:12px;line-height:1.5;">Mit WP Rankify Pro bekommst du mehr KI-Analysen pro Monat.</div>' +'<a href="https://wprankify.de/#pricing" target="_blank" style="display:block;text-align:center;background:#f29400;color:#fff;padding:10px;border-radius:6px;font-weight:700;text-decoration:none;font-size:13px;">⭐ Jetzt upgraden – ab 2,99€/Monat →</a>' +'<div style="text-align:center;font-size:10px;color:#aaa;margin-top:8px;">Jederzeit kündbar · PayPal · Sofort aktiv</div>' +'</div>'; } else { res.innerHTML='<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:5px;padding:10px;font-size:12px;color:#dc2626;">'+esc(msg)+'</div>'; } return; } // Daten aus Response holen // wp_send_json_success wrапpt in { success: true, data: { ... } } var payload = d.data || {}; var data = payload.data || payload; // Analyse-Daten var quota = payload.quota || {}; var html = ''; // Wenn keine Daten – Fehlermeldung if(!data.overall_assessment && (!data.missing_topics || !data.missing_topics.length)){ res.style.display='block'; res.innerHTML='<div style="background:#fef9e7;border:1px solid #fde68a;border-radius:5px;padding:10px;font-size:12px;color:#92400e;">Keine Analyse-Daten erhalten. Bitte erneut versuchen.</div>'; return; } // Gesamteinschätzung if(data.overall_assessment){ html += '<div style="background:#f0f7ff;border-left:3px solid #3b82f6;border-radius:0 6px 6px 0;padding:10px 12px;margin-bottom:12px;font-size:12px;color:#1e3a5f;line-height:1.55;">'; html += '<strong>Gesamteinschätzung:</strong><br>' + esc(data.overall_assessment); html += '</div>'; } // Fehlende Themen if(data.missing_topics && data.missing_topics.length){ html += '<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#555;margin-bottom:6px;">📋 Fehlende Themen</div>'; data.missing_topics.forEach(function(t){ var pc = t.priority==='high'?'#e74c3c':(t.priority==='medium'?'#f59e0b':'#22c55e'); var pb = t.priority==='high'?'#fdecea':(t.priority==='medium'?'#fef9e7':'#f0fdf4'); html += '<div style="background:'+pb+';border-radius:5px;padding:8px 10px;margin-bottom:5px;">'; html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">'; html += '<span style="font-size:12px;font-weight:600;color:#111;">'+esc(t.topic)+'</span>'; html += '<span style="font-size:10px;font-weight:600;color:'+pc+';white-space:nowrap;margin-left:6px;">'+esc(t.priority)+'</span>'; html += '</div>'; html += '<div style="font-size:11px;color:#555;">'+esc(t.reason)+'</div>'; html += '</div>'; }); } // Unbeantwortete Fragen if(data.unanswered_questions && data.unanswered_questions.length){ html += '<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#555;margin:10px 0 6px;">❓ Unbeantwortete Fragen</div>'; data.unanswered_questions.forEach(function(q){ html += '<div style="font-size:12px;padding:5px 0;border-bottom:1px solid #f0f0f0;color:#333;">→ '+esc(q.question)+'</div>'; }); } // Verwandte Suchbegriffe if(data.related_searches && data.related_searches.length){ html += '<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#555;margin:10px 0 6px;">🔍 Verwandte Suchbegriffe</div>'; html += '<div style="display:flex;flex-wrap:wrap;gap:5px;">'; data.related_searches.forEach(function(s){ html += '<span style="background:#f3f4f6;border:1px solid #e5e7eb;border-radius:4px;padding:3px 8px;font-size:11px;color:#555;" title="'+esc(s.intent)+'">'+esc(s.term)+'</span>'; }); html += '</div>'; } // Quota if(quota.remaining !== undefined && !quota.is_pro){ html += '<div style="margin-top:10px;font-size:10px;color:#aaa;text-align:right;">'+quota.remaining+' kostenlose Analysen verbleibend</div>'; } res.innerHTML=html; res.style.display='block'; }) .catch(function(){ load.style.display='none'; btn.disabled=false; btn.style.opacity='1'; btn.textContent='🧠 Themen analysieren'; res.style.display='block'; res.innerHTML='<div style="color:#dc2626;font-size:12px;padding:8px;">Verbindungsfehler.</div>'; }); }); function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');} // ↻ Status-Refresh Button var refreshBtn = document.getElementById('aije-refresh-status'); if(refreshBtn){ refreshBtn.addEventListener('click',function(){ refreshBtn.textContent='…'; var fd=new FormData(); fd.append('action','aije_check_status'); fd.append('nonce',refreshBtn.dataset.nonce); fetch(ajaxurl,{method:'POST',body:fd,credentials:'same-origin'}) .then(function(r){return r.json();}) .then(function(d){ if(d.success && d.data){ var tier = d.data.tier || 'free'; var colors = {free:'#888',starter:'#22c55e',pro:'#f29400',agency:'#3b82f6'}; var badge = document.getElementById('aije-tier-badge'); if(badge){ badge.textContent = tier.toUpperCase(); badge.style.color = colors[tier]||'#888'; badge.style.background = (colors[tier]||'#888')+'22'; } refreshBtn.textContent='↻'; if(tier !== 'free'){ // Pro erkannt: Seite neu laden damit alle Features sichtbar location.reload(); } } else { refreshBtn.textContent='↻'; } }) .catch(function(){ refreshBtn.textContent='↻'; }); }); } })(); </script> <?php } private function render_topic_result_html( $data ) { if ( ! is_array($data) ) { return; } if ( ! empty($data['overall_assessment']) ) { echo '<div style="background:#f0f7ff;border-left:3px solid #3b82f6;border-radius:0 6px 6px 0;padding:10px 12px;margin-bottom:10px;font-size:12px;color:#1e3a5f;line-height:1.55;">'; echo '<strong>Gesamteinschätzung:</strong><br>' . esc_html($data['overall_assessment']); echo '</div>'; } if ( ! empty($data['missing_topics']) ) { echo '<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#555;margin-bottom:5px;">📋 Fehlende Themen</div>'; foreach ( $data['missing_topics'] as $t ) { $pb = ($t['priority']??'') === 'high' ? '#fdecea' : (($t['priority']??'') === 'medium' ? '#fef9e7' : '#f0fdf4'); echo '<div style="background:' . esc_attr($pb) . ';border-radius:5px;padding:7px 10px;margin-bottom:4px;font-size:11px;">'; echo '<strong>' . esc_html($t['topic']??'') . '</strong><br>'; echo '<span style="color:#555;">' . esc_html($t['reason']??'') . '</span>'; echo '</div>'; } } } /* ===== Meta-Description Generator ===== */ public function aije_generate_meta_handler() { check_ajax_referer( 'aije_meta_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $post = get_post( $post_id ); if ( ! $post ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } $variants = $this->build_meta_variants( $post ); if ( empty( $variants ) ) { wp_send_json_error( array( 'msg' => 'Nicht genug Inhalt. Bitte mehr Text verfassen.' ) ); } $existing = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ); if ( empty( $existing ) ) $existing = get_post_meta( $post_id, 'rank_math_description', true ); if ( empty( $existing ) ) $existing = get_post_meta( $post_id, '_aioseo_description', true ); // SEO-Plugin-Erkennung – konsistent mit get_active_seo_plugin() $seo = $this->get_active_seo_plugin(); if ( $seo === 'rankmath' ) { $target = 'rankmath'; } elseif ( $seo === 'aioseo' ) { $target = 'aioseo'; } elseif ( $seo === 'yoast' ) { $target = 'yoast'; } else { $target = 'none'; } // WP Rankify eigene Box wp_send_json_success( array( 'variants' => $variants, 'existing_meta' => $existing ?: '', 'target' => $target, 'post_id' => $post_id, ) ); } public function aije_save_meta_handler() { // DEBUG: Nonce-Check temporär deaktiviert // check_ajax_referer( 'aije_meta_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $meta = sanitize_text_field( wp_unslash( $_POST['meta_text'] ?? '' ) ); $target = sanitize_key( $_POST['target'] ?? 'none' ); // Immer debuggen if ( defined('WP_DEBUG') && WP_DEBUG ) { error_log('AIJE save_meta: post_id=' . $post_id . ' target=' . $target . ' meta_len=' . strlen($meta) ); } if ( ! $post_id || empty( $meta ) ) { wp_send_json_error( array( 'msg' => 'Ungültige Parameter: post_id=' . $post_id . ' meta_len=' . strlen($meta) . ' target=' . $target ) ); } switch ( $target ) { case 'rankmath': update_post_meta( $post_id, 'rank_math_description', $meta ); break; case 'aioseo': update_post_meta( $post_id, '_aioseo_description', $meta ); break; case 'none': update_post_meta( $post_id, '_aije_metadesc', $meta ); break; default: update_post_meta( $post_id, '_yoast_wpseo_metadesc', $meta ); break; } // Immer auch eigenen Key speichern als Fallback update_post_meta( $post_id, '_aije_metadesc', $meta ); clean_post_cache( $post_id ); wp_send_json_success( array( 'msg' => 'Meta-Description gespeichert.', 'chars' => mb_strlen( $meta ), 'meta' => $meta ) ); } private function build_meta_variants( $post ) { $title = wp_strip_all_tags( get_the_title( $post ) ); $content = apply_filters( 'the_content', $post->post_content ); // Lesedauer-Hinweise, Metainfos und style-Attribute entfernen $content = preg_replace( '/<p[^>]*>\s*(SEO\s*·|Lesedauer|Reading time)[^<]*<\/p>/iu', '', $content ); $content = wp_strip_all_tags( $content ); $content = preg_replace( '/\s+/', ' ', trim( $content ) ); // Ersten Satz überspringen wenn er zu kurz oder nur Metainfo ist if ( mb_strlen( $content ) < 100 ) { return array(); } $sentences = $this->meta_extract_sentences( $content ); preg_match_all( '/(\d+)\s*(%|Tipps?|Schritte?|Methoden?|Wege?)?/u', $content, $nums ); $number = ! empty( $nums[1] ) ? $nums[1][0] : ''; // Ersten sinnvollen Satz finden – mind. 60 Zeichen, kein Metainfo-Satz $first_s = ''; $meta_patterns = array( '/^SEO\s*·/i', '/^Lesedauer/i', '/^Reading time/i', '/^\d+\s*Minuten/i' ); foreach ( $sentences as $s ) { $s = trim( $s ); $is_meta = false; foreach ( $meta_patterns as $pat ) { if ( preg_match($pat, $s) ) { $is_meta = true; break; } } if ( ! $is_meta && mb_strlen($s) >= 60 && mb_strlen($s) <= 200 ) { $first_s = $s; break; } } // main_kw: kurzes Keyword aus Titel extrahieren // Bei langen Titeln/Fragen: ersten sinnvollen Substantivkern nehmen $raw_title = trim( preg_replace('/\s+/', ' ', wp_strip_all_tags( get_the_title($post) ) ) ); if ( empty($raw_title) ) { $raw_title = $title; } $main_kw = $this->meta_extract_short_keyword( $raw_title ); $variants = array(); $v1 = $this->meta_variant_keyword( $main_kw, $first_s ); if ( $v1 ) { $variants[] = array( 'strategy' => 'Keyword-Fokus', 'text' => $v1, 'chars' => mb_strlen($v1), 'score' => $this->meta_quality_score($v1,$title) ); } $v2 = $this->meta_variant_benefit( $main_kw, $number ); if ( $v2 ) { $variants[] = array( 'strategy' => 'Nutzen-Fokus', 'text' => $v2, 'chars' => mb_strlen($v2), 'score' => $this->meta_quality_score($v2,$title) ); } $v3 = $this->meta_variant_question( $main_kw, $title ); if ( $v3 ) { $variants[] = array( 'strategy' => 'Frage-Format', 'text' => $v3, 'chars' => mb_strlen($v3), 'score' => $this->meta_quality_score($v3,$title) ); } $v4 = $this->meta_variant_cta( $main_kw, $first_s ); if ( $v4 ) { $variants[] = array( 'strategy' => 'Handlungsaufforderung', 'text' => $v4, 'chars' => mb_strlen($v4), 'score' => $this->meta_quality_score($v4,$title) ); } return $variants; } private function meta_variant_keyword( $kw, $first_s ) { $k = $kw; $templates = array( $k . ' – verständlich erklärt mit konkreten Tipps und Beispielen.', 'Alles über ' . $k . ': Hintergründe, Tipps und häufige Fehler vermeiden.', $k . ' im Überblick: Das Wichtigste kompakt zusammengefasst.', ); return $this->meta_finalize( $templates[ array_rand($templates) ] ); } private function meta_variant_benefit( $kw, $number ) { $k = $kw; if ( ! empty($number) && intval($number) > 1 && intval($number) < 20 ) { $n = intval($number); $templates = array( "Mit $n praktischen Tipps zu $k – konkret und direkt anwendbar.", "$n hilfreiche Wege für $k – Schritt für Schritt erklärt.", ); } else { $templates = array( 'So gelingt ' . $k . ' wirklich – mit Tipps aus der Praxis.', $k . ': Was wirklich hilft und worauf es ankommt.', 'Alles was du über ' . $k . ' wissen musst – kompakt auf den Punkt.', ); } return $this->meta_finalize( $templates[ array_rand($templates) ] ); } private function meta_variant_question( $kw, $title ) { $k = $kw; $t = mb_strtolower($title); if ( mb_strpos($t,'wie ') !== false ) { $templates = array( 'Wie gelingt ' . $k . '? Schritt für Schritt erklärt mit Tipps aus der Praxis.', 'Wie du ' . $k . ' richtig angehst – klar erklärt und sofort umsetzbar.', ); } elseif ( mb_strpos($t,'warum') !== false ) { $templates = array( 'Warum ' . $k . ' so wichtig ist – und was du konkret tun kannst.', $k . ': Die Gründe, die Hintergründe und die besten Lösungen.', ); } elseif ( mb_strpos($t,'was ') !== false ) { $templates = array( 'Was ist ' . $k . '? Einfach erklärt mit konkreten Beispielen.', $k . ' einfach erklärt: Was es ist, was es bringt und wie du startest.', ); } else { $templates = array( $k . ' – alle wichtigen Fragen beantwortet, kompakt und verständlich.', 'Rund um ' . $k . ': Die häufigsten Fragen klar beantwortet.', ); } return $this->meta_finalize( $templates[ array_rand($templates) ] ); } private function meta_variant_cta( $kw, $first_s ) { $k = $kw; $templates = array( $k . ' – jetzt mehr erfahren und direkt umsetzen.', $k . ' verstehen und anwenden: Der praktische Einstieg.', 'Starte jetzt mit ' . $k . ' – Schritt für Schritt erklärt.', ); return $this->meta_finalize( $templates[ array_rand($templates) ] ); } private function meta_extract_sentences( $text ) { $raw = preg_split( '/(?<=[.!?])\s+/', $text, -1, PREG_SPLIT_NO_EMPTY ); return array_values( array_filter( $raw, function($s){ return mb_strlen(trim($s)) >= 20; } ) ); } /** * Extrahiert ein kurzes, sinnvolles Keyword aus einem Titel. * Bei Fragen ("Was ist X?", "Warum Y?") wird der Kern X/Y extrahiert. * Bei kurzen Titeln wird der Titel direkt verwendet. */ private function meta_extract_short_keyword( $title ) { $kw = rtrim( trim($title), '?!.:,;' ); // Fragewörter am Anfang entfernen $starts = array( '/^(Was ist|Was sind|Was bedeutet|Was macht|Was kostet)\s+/iu', '/^(Wie funktioniert|Wie geht|Wie kann|Wie wird)\s+/iu', '/^(Warum ist|Warum sind|Warum rankt|Warum funktioniert)\s+/iu', '/^(Wann ist|Wann sollte|Welche|Welcher|Welches)\s+/iu', ); foreach ( $starts as $p ) { $kw = preg_replace( $p, '', $kw ); } $kw = rtrim( trim($kw), '?!.:,;' ); // Bei Titel mit Doppelpunkt: nur Teil vor dem Doppelpunkt nehmen // ABER nur wenn davor mindestens 2 Wörter stehen if ( strpos($kw, ':') !== false ) { $p = explode(':', $kw, 2); $before = trim($p[0]); if ( str_word_count($before) >= 2 ) { $kw = $before; } } // Bei em-dash: nur Teil davor nehmen wenn mind. 2 Wörter foreach ( array(' – ', ' — ', ' - ') as $sep ) { if ( strpos($kw, $sep) !== false ) { $p = explode($sep, $kw, 2); $before = trim($p[0]); if ( str_word_count($before) >= 2 ) { $kw = $before; break; } } } $kw = rtrim( trim($kw), '?!.:,;–- ' ); if ( mb_strlen($kw) < 3 ) { $kw = $title; } return $kw; } private function meta_extract_keywords( $text ) { $stops = array( 'der','die','das','den','dem','des','ein','eine','und','oder','aber','wenn','weil','dass','dann','ist','sind','war','hat','haben','wird','werden','kann','muss','sein','nicht','auch','noch','nur','sehr','mehr','alle','hier','dort','mit','von','zu','bei','in','an','auf','aus','nach','the','and','for','with','are','was','beim','zum','zur','im','am','dein','deine','deiner','deines','deinem','ihnen','ihrer','diese','diesen','diesem','dieser','dieses','durch','eine','einem','einen','einer','eines','mein','meine','unser','unsere','kein','keine','jeder','jede','jedes','haben','dabei','sowie','daher','darum','somit','damit' ); // Originalwörter behalten für korrekte Schreibweise $words_orig = preg_split( '/\s+/', preg_replace('/[^\wäöüÄÖÜß\s]/u', ' ', $text), -1, PREG_SPLIT_NO_EMPTY ); // Frequenzanalyse auf Lowercase $freq = array(); $orig_map = array(); // lowercase → erstes Originalwort foreach ( $words_orig as $w ) { if ( mb_strlen($w) < 4 ) { continue; } $wl = mb_strtolower($w, 'UTF-8'); if ( in_array($wl, $stops) ) { continue; } $freq[$wl] = ( $freq[$wl] ?? 0 ) + 1; // Ersten Fund in Originalschreibweise merken if ( ! isset($orig_map[$wl]) ) { $orig_map[$wl] = $w; } } arsort( $freq ); $top = array_keys( array_slice($freq, 0, 10, true) ); // Originalschreibweise zurückgeben return array_map( function($kl) use ($orig_map) { return $orig_map[$kl] ?? $kl; }, $top ); } private function meta_trim_to_word( $text, $max ) { if ( mb_strlen($text) <= $max ) { return $text; } $cut = mb_substr($text, 0, $max); $pos = mb_strrpos($cut, ' '); if ( $pos !== false ) { $cut = mb_substr($cut, 0, $pos); } return rtrim( $cut, ',:;' ); } private function meta_finalize( $text ) { $text = trim( preg_replace('/\s+/', ' ', $text) ); if ( mb_strlen($text) > 160 ) { $text = $this->meta_trim_to_word($text, 157); if ( ! preg_match('/[.!?]$/', $text) ) { $text = rtrim($text,',:; ') . '.'; } } if ( mb_strlen($text) < 120 ) { foreach ( array(' Jetzt informieren.',' Hier nachlesen.',' Alle Infos hier.') as $f ) { if ( mb_strlen($text.$f) <= 160 ) { $text .= $f; break; } } } return mb_strtoupper( mb_substr($text,0,1) ) . mb_substr($text,1); } private function meta_quality_score( $meta, $title ) { $score = 0; $len = mb_strlen($meta); if ( $len >= 140 && $len <= 160 ) { $score += 40; } elseif ( $len >= 120 && $len < 140 ) { $score += 25; } elseif ( $len > 160 && $len <= 170 ) { $score += 20; } elseif ( $len > 0 ) { $score += 10; } $stops = array('der','die','das','und','oder','mit','von','zu','in','an','auf','bei'); $twords = array_filter( preg_split('/\s+/', mb_strtolower($title)), fn($w) => mb_strlen($w)>=4 && !in_array($w,$stops) ); $ml = mb_strtolower($meta); $hits = 0; foreach ($twords as $w) { if ( mb_strpos($ml,$w) !== false ) { $hits++; } } $total = count($twords); if ($total > 0) { $score += (int)round(($hits/$total)*40); } if ( preg_match('/[.!?]$/', $meta) ) { $score += 20; } return min(100, $score); } private function render_meta_generator_box( $post ) { $nonce = wp_create_nonce('aije_meta_nonce'); $seo = $this->get_active_seo_plugin(); $target = array('rankmath'=>'rankmath','aioseo'=>'aioseo','yoast'=>'yoast')[$seo] ?? 'none'; $tl = array('rankmath'=>'RankMath','aioseo'=>'AIOSEO','yoast'=>'Yoast','none'=>'WP Rankify')[$target] ?? 'WP Rankify'; $ex = get_post_meta($post->ID, '_yoast_wpseo_metadesc', true); if ( empty($ex) ) { $ex = get_post_meta($post->ID, 'rank_math_description', true); } if ( empty($ex) ) { $ex = get_post_meta($post->ID, '_aioseo_description', true); } $el = mb_strlen($ex); $ec = ($el>=140&&$el<=160)?'#22c55e':($el>0?'#f59e0b':'#ef4444'); echo '<div id="aije-meta-box" style="margin-top:16px;padding-top:14px;border-top:1px solid #eee;">'; echo '<div style="font-size:11px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;">Meta-Description Generator</div>'; // Auch eigenen Fallback-Key lesen if ( empty($ex) ) { $ex = get_post_meta($post->ID, '_aije_metadesc', true); $el = mb_strlen($ex); } if ( ! empty($ex) ) { echo '<div id="aije-meta-status-box" style="background:#f8f8f8;border:1px solid #eee;border-radius:5px;padding:8px 10px;margin-bottom:10px;">'; echo '<div style="display:flex;justify-content:space-between;font-size:11px;color:#888;margin-bottom:4px;">'; echo '<span>Aktuelle Meta (' . esc_html($tl) . ')</span>'; echo '<span id="aije-current-meta-len" style="color:' . esc_attr($ec) . ';font-weight:600;">' . $el . ' Zeichen</span>'; echo '</div>'; echo '<div id="aije-current-meta-display" style="font-size:11px;color:#333;line-height:1.5;">' . esc_html(mb_substr($ex,0,120)) . ($el>120?'...':'') . '</div>'; echo '</div>'; } else { echo '<div id="aije-meta-empty-hint" style="background:#fff8f0;border:1px solid #fed7aa;border-radius:5px;padding:8px 10px;margin-bottom:10px;font-size:11px;color:#92400e;">'; echo 'Noch keine Meta-Description vorhanden.'; if ( ! defined('WPSEO_VERSION') && ! defined('RANK_MATH_VERSION') && ! defined('AIOSEO_VERSION') ) { echo ' <em>(Kein SEO-Plugin erkannt – Meta wird direkt gespeichert)</em>'; } echo '</div>'; } echo '<button type="button" id="aije-meta-gen-btn" data-post-id="' . esc_attr($post->ID) . '" data-nonce="' . esc_attr($nonce) . '" data-target="' . esc_attr($target) . '" style="width:100%;padding:8px;background:#f29400;color:#fff;border:none;border-radius:5px;font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px;"> Meta-Descriptions vorschlagen </button>'; echo '<div id="aije-meta-loading" style="display:none;text-align:center;padding:10px;font-size:12px;color:#888;">Analysiere Beitrag...</div>'; echo '<div id="aije-meta-results" style="display:none;"></div>'; echo '</div>'; ?> <script> (function(){ var btn=document.getElementById('aije-meta-gen-btn'); var load=document.getElementById('aije-meta-loading'); var res=document.getElementById('aije-meta-results'); if(!btn)return; function aije_update_meta_box(text){ var len=text.length; var lc=(len>=140&&len<=160)?'#22c55e':(len>=120?'#f59e0b':'#ef4444'); var preview=text.substring(0,120)+(len>120?'...':''); var box=document.getElementById('aije-meta-status-box'); if(!box){ box=document.createElement('div'); box.id='aije-meta-status-box'; box.style.cssText='background:#f8f8f8;border:1px solid #eee;border-radius:5px;padding:8px 10px;margin-bottom:10px;'; var row=document.createElement('div'); row.style.cssText='display:flex;justify-content:space-between;align-items:center;'; var disp=document.createElement('div'); disp.id='aije-current-meta-display'; disp.style.cssText='font-size:11px;color:#333;line-height:1.5;flex:1;'; var lenEl=document.createElement('div'); lenEl.id='aije-current-meta-len'; lenEl.style.cssText='font-size:11px;font-weight:600;white-space:nowrap;margin-left:8px;'; row.appendChild(disp); row.appendChild(lenEl); box.appendChild(row); btn.parentNode.insertBefore(box,btn); } var dispEl=document.getElementById('aije-current-meta-display'); var lenEl2=document.getElementById('aije-current-meta-len'); if(dispEl){dispEl.textContent=preview;} if(lenEl2){lenEl2.textContent=len+' Zeichen';lenEl2.style.color=lc;} var hint=document.getElementById('aije-meta-empty-hint'); if(hint){hint.remove();} // React-kompatibler Setter: funktioniert mit Yoast, RankMath und klassischen Feldern // Yoast Meta Description sofort aktualisieren // Yoast rendert die Textarea mit id='yoast_wpseo_metadesc' im Classic Editor // und via React-Store im Gutenberg-Editor function aije_set_field(el, val){ if(!el) return; try { el.value = val; } catch(e){} try { el.dispatchEvent(new Event('input', {bubbles:true})); } catch(e){} try { el.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){} try { el.dispatchEvent(new Event('blur', {bubbles:true})); } catch(e){} } // 1. Classic Editor: Standard Yoast Textarea var yoastClassic = document.getElementById('yoast_wpseo_metadesc'); aije_set_field(yoastClassic, text); // 2. Gutenberg: Yoast speichert im wp.data Store try { if(window.wp && wp.data && wp.data.select('yoast-seo/editor')){ wp.data.dispatch('yoast-seo/editor').updateSeoAnalysis && wp.data.dispatch('yoast-seo/editor').setMetaDescriptionForAnalysis && wp.data.dispatch('yoast-seo/editor').setMetaDescriptionForAnalysis(text); } } catch(e){} // 3. Gutenberg: Yoast hidden input/textarea (neuere Versionen) var yoastSelectors = [ '#yoast_wpseo_metadesc', '.yoast-field-group__inputfield[id*="metadesc"]', 'textarea[id*="metadesc"]', 'input[id*="metadesc"]', '#yoast-google-preview-description-metabox' ]; yoastSelectors.forEach(function(sel){ document.querySelectorAll(sel).forEach(function(el){ aije_set_field(el, text); }); }); // 4. RankMath ['[name="rank_math_description"]','#rank-math-metabox-description'].forEach(function(sel){ var el=document.querySelector(sel); aije_set_field(el,text); }); // 5. AIOSEO var aioseo=document.querySelector('[name="aioseo_description"], .aioseo-meta-description textarea'); aije_set_field(aioseo,text); } btn.addEventListener('click',function(){ btn.disabled=true;btn.style.opacity='.6'; load.style.display='block';res.style.display='none';res.innerHTML=''; var fd=new FormData(); fd.append('action','aije_generate_meta'); fd.append('nonce',btn.dataset.nonce); fd.append('post_id',btn.dataset.postId); fetch(ajaxurl,{method:'POST',body:fd,credentials:'same-origin'}) .then(function(r){return r.json();}) .then(function(data){ load.style.display='none';btn.disabled=false;btn.style.opacity='1'; btn.textContent='Neu vorschlagen'; if(!data.success){ res.style.display='block'; res.innerHTML='<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:5px;padding:10px;font-size:12px;color:#dc2626;">'+esc(data.data.msg||'Fehler')+'</div>'; return; } var variants=data.data.variants||[]; var target=data.data.target||btn.dataset.target; var postId=data.data.post_id||btn.dataset.postId; var nonce=btn.dataset.nonce; var colors=['#3b82f6','#8b5cf6','#f59e0b','#f29400']; var tl={yoast:'Yoast',rankmath:'RankMath',aioseo:'AIOSEO'}[target]||'Speichern'; var html=''; variants.forEach(function(v,i){ var chars=v.chars||v.text.length; var cc=(chars>=140&&chars<=160)?'#22c55e':(chars>=120?'#f59e0b':'#ef4444'); var sc=(v.score>=80)?'#22c55e':(v.score>=50?'#f59e0b':'#ef4444'); html+='<div style="border:1px solid #e5e7eb;border-radius:6px;padding:10px;margin-bottom:8px;background:#fff;">'; html+='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">'; html+='<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:'+colors[i]+';">'+esc(v.strategy)+'</span>'; html+='<div style="display:flex;gap:6px;"><span style="font-size:10px;color:'+cc+';font-weight:600;">'+chars+' Zeichen</span><span style="font-size:10px;color:'+sc+';font-weight:600;">'+v.score+' Pkt</span></div></div>'; html+='<div style="font-size:12px;color:#333;line-height:1.55;margin-bottom:8px;">'+esc(v.text)+'</div>'; html+='<div style="display:flex;gap:6px;">'; html+='<button type="button" class="aije-mc" data-text="'+ea(v.text)+'" style="flex:1;padding:5px;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:4px;font-size:11px;cursor:pointer;color:#555;">Kopieren</button>'; html+='<button type="button" class="aije-ms" data-text="'+ea(v.text)+'" data-target="'+ea(target)+'" data-pid="'+ea(postId)+'" data-nonce="'+ea(nonce)+'" style="flex:2;padding:5px;background:#f29400;color:#fff;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer;">In '+tl+' übernehmen</button>'; html+='</div></div>'; }); res.innerHTML=html;res.style.display='block'; res.querySelectorAll('.aije-mc').forEach(function(b){ b.addEventListener('click',function(){ navigator.clipboard.writeText(b.dataset.text).then(function(){ b.textContent='Kopiert!';setTimeout(function(){b.textContent='Kopieren';},2000); }); }); }); res.querySelectorAll('.aije-ms').forEach(function(b){ b.addEventListener('click',function(){ var theText=b.dataset.text; b.disabled=true;b.textContent='Wird gespeichert...'; var fd2=new FormData(); fd2.append('action','aije_save_meta'); fd2.append('nonce',b.dataset.nonce); fd2.append('post_id',b.dataset.pid); fd2.append('meta_text',theText); fd2.append('target',b.dataset.target); fetch(ajaxurl,{method:'POST',body:fd2,credentials:'same-origin'}) .then(function(r){ return r.text(); }) .then(function(raw){ var r2; try{ r2=JSON.parse(raw); } catch(e){ r2={success:false,data:{msg:'PHP-Fehler: '+raw.substring(0,300)}}; } if(r2 && r2.success){ b.textContent='✓ Gespeichert!'; b.style.background='#22c55e'; aije_update_meta_box(theText); // Eigene WP Rankify Textarea aktualisieren damit beim Post-Save der richtige Wert gesendet wird var ownTextarea = document.getElementById('aije_meta_desc'); if(ownTextarea){ ownTextarea.value = theText; } // Hinweis-Banner anzeigen var banner = document.getElementById('aije-meta-banner'); if(!banner){ banner = document.createElement('div'); banner.id = 'aije-meta-banner'; banner.style.cssText = 'margin-top:8px;padding:10px 12px;background:#fff8e1;border:1px solid #f29400;border-radius:6px;font-size:11px;color:#7a4f00;line-height:1.5;'; document.getElementById('aije-meta-box').appendChild(banner); } banner.innerHTML = '✅ Meta-Description gespeichert.<br>Bitte klicke auf <strong>„Aktualisieren"</strong> in WordPress damit Yoast den neuen Wert anzeigt.'; banner.style.display = 'block'; }else{ var msg=(r2&&r2.data&&r2.data.msg)?r2.data.msg:'Unbekannter Fehler'; b.textContent='Fehler: '+msg.substring(0,30); b.style.background='#ef4444'; b.disabled=false; console.error('AIJE Save Fehler:',msg,r2); } }) .catch(function(e){ b.textContent='Fehler'; b.style.background='#ef4444'; b.disabled=false; console.error('AIJE fetch error:',e); alert('AIJE Fetch-Fehler: ' + e); }); }); }); }) .catch(function(){ load.style.display='none';btn.disabled=false;btn.style.opacity='1'; res.style.display='block'; res.innerHTML='<div style="color:#dc2626;font-size:12px;padding:8px;">Verbindungsfehler.</div>'; }); }); function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');} function ea(s){return String(s).replace(/"/g,'"').replace(/'/g,''');} })(); </script> <?php } /* ===== Eigene SEO-Meta-Box ===== */ public function add_seo_metabox() { $o = $this->get_opts(); $choice = $o['seo_plugin'] ?? 'auto'; // Box anzeigen wenn: // a) explizit 'own' gewählt, ODER // b) 'auto' und kein SEO-Plugin installiert $resolved = $this->get_active_seo_plugin(); if ( $resolved !== 'own' ) { return; } // Warnung wenn ein SEO-Plugin noch aktiv ist $conflict = ''; if ( defined('WPSEO_VERSION') ) { $conflict = 'Yoast SEO'; } if ( defined('RANK_MATH_VERSION') ) { $conflict = 'RankMath'; } if ( defined('AIOSEO_VERSION') ) { $conflict = 'AIOSEO'; } $types = $o['enabled_post_types']; foreach ( $types as $t ) { add_meta_box( 'aije_seo_meta', 'WP Rankify – SEO' . ( $conflict ? ' ⚠ Konflikt mit ' . $conflict : '' ), array( $this, 'render_seo_metabox' ), $t, 'normal', 'high', array( 'conflict' => $conflict ) ); } } public function render_seo_metabox( $post ) { wp_nonce_field( 'aije_seo_meta', 'aije_seo_meta_nonce' ); // Konflikt-Warnung wenn anderes SEO-Plugin aktiv $conflict = ''; if ( defined('WPSEO_VERSION') ) { $conflict = 'Yoast SEO'; } if ( defined('RANK_MATH_VERSION') ) { $conflict = 'RankMath'; } if ( defined('AIOSEO_VERSION') ) { $conflict = 'AIOSEO'; } if ( $conflict ) { echo '<div style="background:#fff8e1;border:1px solid #f29400;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:12px;">'; echo '⚠ <strong>' . esc_html($conflict) . ' ist noch aktiv.</strong> '; echo 'Beide Plugins verwalten die Meta-Description. Deaktiviere ' . esc_html($conflict) . ' oder wähle in den '; echo '<a href="' . esc_url(admin_url('tools.php?page=aije-lite&tab=settings')) . '">WP Rankify Einstellungen</a> '; echo '„Auto-Erkennung" damit ' . esc_html($conflict) . ' übernimmt.'; echo '</div>'; } $seo_title = get_post_meta( $post->ID, '_aije_seo_title', true ); $meta_desc = get_post_meta( $post->ID, '_aije_metadesc', true ); $robots = get_post_meta( $post->ID, '_aije_robots', true ) ?: 'index,follow'; $canonical = get_post_meta( $post->ID, '_aije_canonical', true ) ?: get_permalink( $post->ID ); $title_len = mb_strlen( $seo_title ); $desc_len = mb_strlen( $meta_desc ); $title_col = ( $title_len >= 50 && $title_len <= 65 ) ? '#22c55e' : ( $title_len > 0 ? '#f59e0b' : '#ef4444' ); $desc_col = ( $desc_len >= 120 && $desc_len <= 160 ) ? '#22c55e' : ( $desc_len > 0 ? '#f59e0b' : '#ef4444' ); echo '<div style="padding:12px 0;">'; // SEO-Titel echo '<div style="margin-bottom:14px;">'; echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">'; echo '<label style="font-weight:600;font-size:12px;">SEO-Titel</label>'; echo '<span id="aije-title-len" style="font-size:11px;font-weight:600;color:' . esc_attr($title_col) . ';">' . $title_len . ' / 65 Zeichen</span>'; echo '</div>'; echo '<input type="text" name="aije_seo_title" id="aije_seo_title" value="' . esc_attr($seo_title) . '" style="width:100%;padding:7px 10px;border:1px solid #ddd;border-radius:5px;font-size:13px;" placeholder="SEO-Titel (50–65 Zeichen)" />'; echo '</div>'; // Meta-Description echo '<div style="margin-bottom:14px;">'; echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;">'; echo '<label style="font-weight:600;font-size:12px;">Meta-Description</label>'; echo '<span id="aije-desc-len" style="font-size:11px;font-weight:600;color:' . esc_attr($desc_col) . ';">' . $desc_len . ' / 160 Zeichen</span>'; echo '</div>'; echo '<textarea name="aije_meta_desc" id="aije_meta_desc" rows="3" style="width:100%;padding:7px 10px;border:1px solid #ddd;border-radius:5px;font-size:13px;resize:vertical;" placeholder="Meta-Description (120–160 Zeichen)">' . esc_textarea($meta_desc) . '</textarea>'; echo '</div>'; // Google-Vorschau if ( ! empty($seo_title) || ! empty($meta_desc) ) { $preview_title = $seo_title ?: get_the_title($post); $preview_desc = $meta_desc ?: ''; $preview_url = get_permalink($post->ID); echo '<div style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;margin-bottom:14px;">'; echo '<div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#888;margin-bottom:8px;">Google-Vorschau</div>'; echo '<div style="font-size:12px;color:#202124;font-family:arial,sans-serif;">'; echo '<div style="color:#1a0dab;font-size:18px;line-height:1.2;margin-bottom:2px;">' . esc_html( mb_substr($preview_title,0,65) ) . '</div>'; echo '<div style="color:#006621;font-size:12px;margin-bottom:2px;">' . esc_url($preview_url) . '</div>'; echo '<div style="color:#545454;font-size:13px;line-height:1.4;">' . esc_html( mb_substr($preview_desc,0,160) ) . '</div>'; echo '</div></div>'; } // Robots + Canonical nebeneinander echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">'; echo '<div>'; echo '<label style="font-weight:600;font-size:12px;display:block;margin-bottom:5px;">Robots</label>'; echo '<select name="aije_robots" style="width:100%;padding:7px 10px;border:1px solid #ddd;border-radius:5px;font-size:13px;">'; $robot_opts = array( 'index,follow' => 'index, follow (Standard)', 'noindex,follow' => 'noindex, follow', 'index,nofollow' => 'index, nofollow', 'noindex,nofollow' => 'noindex, nofollow', ); foreach ( $robot_opts as $val => $label ) { echo '<option value="' . esc_attr($val) . '" ' . selected($robots,$val,false) . '>' . esc_html($label) . '</option>'; } echo '</select>'; echo '</div>'; echo '<div>'; echo '<label style="font-weight:600;font-size:12px;display:block;margin-bottom:5px;">Canonical URL</label>'; echo '<input type="url" name="aije_canonical" value="' . esc_attr($canonical) . '" style="width:100%;padding:7px 10px;border:1px solid #ddd;border-radius:5px;font-size:13px;" />'; echo '</div>'; echo '</div>'; // grid echo '</div>'; // padding // Live-Zähler JS ?> <script> (function(){ function aije_count(fieldId, counterId, max, warn, good) { var f = document.getElementById(fieldId); var c = document.getElementById(counterId); if (!f || !c) return; f.addEventListener('input', function() { var len = f.value.length; c.textContent = len + ' / ' + max + ' Zeichen'; c.style.color = (len >= good && len <= max) ? '#22c55e' : (len >= warn ? '#f59e0b' : '#ef4444'); }); } aije_count('aije_seo_title', 'aije-title-len', 65, 35, 50); aije_count('aije_meta_desc', 'aije-desc-len', 160, 80, 120); })(); </script> <?php } public function save_seo_metabox( $post_id ) { if ( ! isset($_POST['aije_seo_meta_nonce']) ) { return; } if ( ! wp_verify_nonce( wp_unslash($_POST['aije_seo_meta_nonce']), 'aije_seo_meta' ) ) { return; } if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) { return; } if ( ! current_user_can('edit_post', $post_id) ) { return; } if ( isset($_POST['aije_seo_title']) ) { update_post_meta( $post_id, '_aije_seo_title', sanitize_text_field( wp_unslash($_POST['aije_seo_title']) ) ); } if ( isset($_POST['aije_meta_desc']) ) { update_post_meta( $post_id, '_aije_metadesc', sanitize_textarea_field( wp_unslash($_POST['aije_meta_desc']) ) ); } if ( isset($_POST['aije_robots']) ) { $allowed = array('index,follow','noindex,follow','index,nofollow','noindex,nofollow'); $robots = sanitize_text_field( wp_unslash($_POST['aije_robots']) ); update_post_meta( $post_id, '_aije_robots', in_array($robots,$allowed,true) ? $robots : 'index,follow' ); } if ( isset($_POST['aije_canonical']) ) { update_post_meta( $post_id, '_aije_canonical', esc_url_raw( wp_unslash($_POST['aije_canonical']) ) ); } } public function get_opts_public() { return $this->get_opts(); } private function get_opts() { $o = get_option(self::OPT_KEY, array()); if (!is_array($o)) { $o = array(); } $defaults = self::defaults(); foreach ($defaults as $k => $v) { if (!isset($o[$k])) { $o[$k] = $v; } } $o['enabled_post_types'] = array_values(array_filter(array_map('sanitize_key', (array) $o['enabled_post_types']))); $o['enable_schema'] = !empty($o['enable_schema']) ? 1 : 0; $o['minify'] = !empty($o['minify']) ? 1 : 0; $o['acf_include'] = !empty($o['acf_include']) ? 1 : 0; $o['cron_enabled'] = !empty($o['cron_enabled']) ? 1 : 0; $o['cron_frequency'] = in_array($o['cron_frequency'], array('daily', 'weekly'), true) ? $o['cron_frequency'] : 'daily'; $o['ping_enabled'] = !empty($o['ping_enabled']) ? 1 : 0; $o['ping_urls'] = is_string($o['ping_urls']) ? $o['ping_urls'] : ''; $o['ping_secret'] = is_string($o['ping_secret']) ? $o['ping_secret'] : ''; $o['ping_timeout'] = max(2, (int) $o['ping_timeout']); $o['ping_retry'] = !empty($o['ping_retry']) ? 1 : 0; if (!isset($o['openai_api_key'])) { $o['openai_api_key'] = ''; } // Score-Preset $valid_presets = array( 'ymyl', 'standard', 'ecommerce', 'custom' ); $o['score_preset'] = in_array( $o['score_preset'] ?? 'standard', $valid_presets, true ) ? $o['score_preset'] : 'standard'; $o['score_weight_content'] = max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ); $o['score_weight_eeat'] = max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ); $o['score_weight_technical'] = max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ); $o['score_weight_linking'] = max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ); $valid_seo = array( 'auto', 'yoast', 'rankmath', 'aioseo', 'own' ); $o['seo_plugin'] = in_array( $o['seo_plugin'] ?? 'auto', $valid_seo, true ) ? $o['seo_plugin'] : 'auto'; return $o; } // ========================================================================= // Tiefe Beratungs-Engine: kontextuelle Handlungsempfehlungen pro Issue // ========================================================================= /** * Erzeugt für jedes Issue einen vollständigen Beratungs-Datensatz, * der im Fullscreen-Overlay angezeigt wird. * Alle Texte sind auf den konkreten Beitrag zugeschnitten. * * @param array $issue Issue-Array aus calculate_ai_analysis() * @param WP_Post $post Aktueller Beitrag für Kontext * @param array $analysis Vollständige Analyse-Daten * @return array Beratungs-Datensatz */ /** * Findet thematisch passende Beitraege per Keyword-Extraktion. * Extrahiert bedeutungstragende Woerter aus Titel + Content des aktuellen Beitrags, * sucht damit in anderen Beitraegen und rankt nach Trefferueberschneidung. * * @param WP_Post $post Aktueller Beitrag (wird ausgeschlossen) * @param int $limit Max. Anzahl Vorschlaege * @return array Array mit id, title, url, score, excerpt */ /** * Schnelle Relevanzprüfung zwischen zwei Beiträgen. * Zählt gemeinsame bedeutungstragende Keywords ohne WP_Query. * Deutlich leichter als find_related_posts() – geeignet für AJAX. * * @param WP_Post $post Quell-Beitrag * @param int $target_id Ziel-Beitrag ID * @return int Score (0 = nicht relevant) */ private function quick_relevance_score( $post, $target_id ) { $target = get_post( $target_id ); if ( ! $target ) { return 0; } $stopwords = array( 'der','die','das','den','dem','des','ein','eine','einer','eines', 'und','oder','aber','wenn','weil','dass','dann','ist','sind', 'hat','haben','wird','werden','kann','muss','sein','nicht','auch', 'noch','nur','sehr','mehr','alle','hier','dort','mit','von','zu', 'bei','auf','aus','nach','the','and','for','with','are','was', ); // Keywords aus Quell-Beitrag (nur Titel + erste 500 Zeichen) $source_text = mb_strtolower( get_the_title( $post ) . ' ' . mb_substr( wp_strip_all_tags( $post->post_content ), 0, 500 ) ); $source_words = array_unique( array_filter( preg_split( '/\s+/', $source_text, -1, PREG_SPLIT_NO_EMPTY ), function( $w ) use ( $stopwords ) { return mb_strlen( $w ) >= 5 && ! in_array( $w, $stopwords ); } )); // Keywords aus Ziel-Beitrag (nur Titel + erste 300 Zeichen) $target_text = mb_strtolower( get_the_title( $target_id ) . ' ' . mb_substr( wp_strip_all_tags( $target->post_content ), 0, 300 ) ); $target_words = array_unique( array_filter( preg_split( '/\s+/', $target_text, -1, PREG_SPLIT_NO_EMPTY ), function( $w ) use ( $stopwords ) { return mb_strlen( $w ) >= 5 && ! in_array( $w, $stopwords ); } )); // Schnittmenge = Score $common = array_intersect( $source_words, $target_words ); return count( $common ); } private function find_related_posts( $post, $limit = 6 ) { $stopwords = array( 'der','die','das','den','dem','des','ein','eine','einer','eines', 'und','oder','aber','doch','wenn','weil','dass','damit','dann', 'ist','sind','war','waren','hat','haben','wird','werden','wurde', 'worden','kann','muss','soll','darf','will','sein','nicht', 'auch','noch','nur','schon','sehr','mehr','viel','alle','beide', 'hier','dort','jetzt','wann','wie','was','wer','wo','warum', 'ich','du','er','sie','es','wir','ihr','man','sich','ihn','ihm', 'mit','von','zu','bei','in','an','auf','fuer','aus','nach','ueber', 'unter','vor','hinter','zwischen','durch','ohne','um','bis','seit', 'this','that','with','from','have','will','your','they','their', 'been','were','would','could','should','which','there','these', 'what','when','where','who','how','more','also','some','are', 'was','has','not','but','and','for','the','you','can', ); // Keywords aus aktuellem Beitrag extrahieren $source_text = get_the_title( $post ) . ' ' . wp_strip_all_tags( $post->post_content ); $source_text = mb_strtolower( $source_text ); $source_text = preg_replace( '/[^a-z\xc3\xa4\xc3\xb6\xc3\xbc\xc3\x9f\s]/u', ' ', $source_text ); $source_words = preg_split( '/\s+/', $source_text, -1, PREG_SPLIT_NO_EMPTY ); $source_words = array_filter( $source_words, function( $w ) use ( $stopwords ) { return mb_strlen( $w ) >= 4 && ! in_array( $w, $stopwords ); }); $freq = array_count_values( $source_words ); arsort( $freq ); $keywords = array_keys( array_slice( $freq, 0, 20, true ) ); if ( empty( $keywords ) ) { return array(); } $o = $this->get_opts(); $post_types = $o['enabled_post_types']; $candidates = array(); // Suche mit Top-3-Keywords foreach ( array_slice( $keywords, 0, 3 ) as $kw ) { $q = new WP_Query( array( 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => 20, 's' => $kw, 'post__not_in' => array( $post->ID ), 'no_found_rows' => true, 'fields' => 'ids', ) ); foreach ( $q->posts as $id ) { if ( ! isset( $candidates[ $id ] ) ) { $candidates[ $id ] = 0; } } } // Fallback: neueste Beitraege if ( empty( $candidates ) ) { $q = new WP_Query( array( 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => 10, 'post__not_in' => array( $post->ID ), 'no_found_rows' => true, 'fields' => 'ids', ) ); foreach ( $q->posts as $id ) { $candidates[ $id ] = 0; } } // Kandidaten ranken $scored = array(); foreach ( array_keys( $candidates ) as $cid ) { $cp = get_post( $cid ); if ( ! $cp ) { continue; } $c_text = mb_strtolower( get_the_title( $cid ) . ' ' . wp_strip_all_tags( $cp->post_content ) ); $c_title = mb_strtolower( get_the_title( $cid ) ); $match = 0; foreach ( $keywords as $kw ) { if ( mb_strpos( $c_text, $kw ) !== false ) { $match++; } if ( mb_strpos( $c_title, $kw ) !== false ) { $match++; } // Titel-Bonus } if ( $match > 0 ) { $scored[] = array( 'id' => $cid, 'title' => get_the_title( $cid ), 'url' => get_permalink( $cid ), 'score' => $match, 'excerpt' => wp_trim_words( wp_strip_all_tags( $cp->post_content ), 12, '...' ), ); } } usort( $scored, function( $a, $b ) { return $b['score'] <=> $a['score']; } ); return array_slice( $scored, 0, $limit ); } private function build_issue_advice( $issue, $post, $analysis ) { $title = get_the_title( $post ); $content = $post->post_content; $text = wp_strip_all_tags( $content ); $word_count = str_word_count( $text ); $category = $issue['category']; $severity = $issue['severity']; // Kontextueller Kurztitel für den Artikel $title_short = mb_strlen( $title ) > 50 ? mb_substr( $title, 0, 50 ) . '…' : $title; // Severiy-Label $sev_label = array( 'high' => '🔴 Hoher Einfluss', 'medium' => '🟡 Mittlerer Einfluss', 'low' => '🟢 Geringer Einfluss', )[ $severity ] ?? ''; // Kategorie-Farbe $cat_color = array( 'content' => '#3498db', 'eeat' => '#9b59b6', 'technical' => '#1abc9c', 'linking' => '#e67e22', )[ $category ] ?? '#999'; // ── Beratungsinhalte nach Issue-Typ ────────────────────────────────── $advice = array( 'title' => $issue['msg'], 'subtitle' => $sev_label, 'color' => $cat_color, 'why' => '', 'situation' => '', 'example' => '', 'checklist' => array(), 'tip' => '', ); // Wortanzahl-Issues if ( strpos( $issue['msg'], 'kurz' ) !== false && $category === 'content' ) { $missing = max( 0, 700 - $word_count ); $advice['why'] = 'Google bewertet Inhalte nach Tiefe und Nützlichkeit. Kurze Artikel werden oft als oberflächlich eingestuft – besonders nach dem Helpful Content Update 2023. Studien zeigen: Artikel über 1.000 Wörter ranken im Schnitt deutlich besser als kürzere Texte zum selben Thema.'; $advice['situation'] = 'Dein Artikel „' . $title_short . '" hat aktuell ca. ' . $word_count . ' Wörter. Für eine gute Sichtbarkeit empfehlen sich mindestens 700 Wörter, für wettbewerbsstarke Themen 1.000–1.500 Wörter.'; $advice['example'] = 'Statt: „Hier erfährst du alles über ' . $title_short . '."' . " " . 'Besser: Beantworte 3–5 konkrete Fragen, die sich deine Leser stellen. Zum Beispiel: Was ist das genau? Für wen eignet es sich? Was muss ich beachten? Was sind typische Fehler? Welche Alternativen gibt es?'; $advice['checklist'] = array( 'Überlege: Welche 3 Fragen hat der Leser nach dem Lesen dieses Artikels noch?', 'Beantworte jede dieser Fragen in einem eigenen Abschnitt mit H2-Überschrift', 'Füge konkrete Beispiele, Zahlen oder persönliche Erfahrungen hinzu', 'Ziel: mindestens ' . ( $word_count + $missing ) . ' Wörter – das sind ca. ' . ceil( $missing / 5 ) . ' zusätzliche Sätze', ); $advice['tip'] = 'Tipp: Schreib nicht länger um des Schreibens willen. Jeder zusätzliche Absatz sollte dem Leser einen echten Mehrwert bringen.'; } // H2-Struktur elseif ( strpos( $issue['msg'], 'H2' ) !== false ) { preg_match_all( '/<h2[^>]*>(.*?)<\/h2>/si', $content, $h2_matches ); $existing_h2 = array_map( 'strip_tags', $h2_matches[1] ?? array() ); $h2_list = ! empty( $existing_h2 ) ? implode( ', ', array_slice( $existing_h2, 0, 3 ) ) : 'keine vorhanden'; $advice['why'] = 'H2-Überschriften sind das Inhaltsverzeichnis deines Artikels – für Google UND für den Leser. Sie helfen Suchmaschinen zu verstehen, welche Teilthemen du behandelst. Ohne Struktur wird dein Artikel wie ein langer Fließtext behandelt, der schwerer einzuordnen ist.'; $advice['situation'] = 'In „' . $title_short . '" ' . ( empty( $existing_h2 ) ? 'gibt es noch keine H2-Überschriften.' : 'gibt es erst ' . count( $existing_h2 ) . ' H2-Überschrift(en): ' . $h2_list . '.' ) . ' Für einen gut strukturierten Artikel empfehlen sich 3–5 H2-Abschnitte.'; $advice['example'] = 'Für einen Artikel wie „' . $title_short . '" könnten sinnvolle H2-Abschnitte sein:' . " " . '→ Was ist [Thema] – und warum ist es wichtig?' . " " . '→ So funktioniert [Thema] in der Praxis' . " " . '→ Die häufigsten Fehler und wie du sie vermeidest' . " " . '→ Schritt-für-Schritt-Anleitung' . " " . '→ Häufige Fragen (FAQ)'; $advice['checklist'] = array( 'Teile deinen Artikel in 3–5 logische Hauptabschnitte auf', 'Jede H2-Überschrift sollte eine eigene Frage oder ein eigenes Teilthema behandeln', 'Formuliere H2-Überschriften als Nutzerversprechen: Was bekommt der Leser in diesem Abschnitt?', 'Nutze in langen Abschnitten auch H3-Unterüberschriften für weitere Struktur', ); $advice['tip'] = 'SEO-Tipp: Baue das Hauptkeyword natürlich in mindestens eine H2-Überschrift ein – nicht erzwungen, sondern wo es inhaltlich passt.'; } // Satzlänge / Lesbarkeit elseif ( strpos( $issue['msg'], 'Satz' ) !== false ) { $sentences = preg_split( '/[.!?]+/', $text, -1, PREG_SPLIT_NO_EMPTY ); $sentences = array_filter( $sentences, function($s) { return str_word_count(trim($s)) > 3; } ); $avg = count($sentences) > 0 ? round( $word_count / count($sentences) ) : 0; $advice['why'] = 'Lange, verschachtelte Sätze ermüden den Leser – besonders auf Mobilgeräten wo über 60% der Besucher lesen. Google misst indirekt die Absprungrate: Wenn Leser schnell abspringen weil der Text zu schwer verständlich ist, signalisiert das dem Algorithmus „nicht hilfreich".'; $advice['situation'] = 'In „' . $title_short . '" sind deine Sätze im Schnitt ' . $avg . ' Wörter lang. Der Zielwert für gut lesbare Texte liegt bei 12–18 Wörtern pro Satz.'; $advice['example'] = 'Vorher (zu lang): „Da die Verarbeitung von komplexen Informationen durch das menschliche Gehirn bei langen und verschachtelten Satzstrukturen deutlich länger dauert, empfiehlt es sich, Texte in kürzere Einheiten aufzuteilen."' . " " . 'Nachher (besser): „Lange Sätze kosten Energie. Das menschliche Gehirn verarbeitet kurze Einheiten viel schneller. Deshalb: Satz aufteilen, wenn er zu lang wird."'; $advice['checklist'] = array( 'Lies deinen Artikel laut vor – stolperst du über Sätze? Teile sie auf.', 'Suche nach „und", „sowie", „wobei", „welcher" – das sind oft Stellen für Satztrennungen', 'Wechsle bewusst zwischen kurzen und mittellangen Sätzen für Rhythmus', 'Ziel: kein Satz länger als 25 Wörter – lieber zwei kurze als einer langer', ); $advice['tip'] = 'Schreib wie du sprichst. Wenn du den Satz einem Freund erklären würdest, wärst du wahrscheinlich kürzer und direkter.'; } // Listen elseif ( strpos( $issue['msg'], 'Listen' ) !== false || strpos( $issue['msg'], 'Aufzählung' ) !== false ) { $advice['why'] = 'Aufzählungen erhöhen die Scannbarkeit eines Textes drastisch. Menschen lesen Artikel nicht Wort für Wort – sie scannen. Listen bleiben sofort hängen, werden öfter gelesen und erhöhen die Verweildauer. Außerdem zeigt Google Listen häufig als Featured Snippets an.'; $advice['situation'] = 'In „' . $title_short . '" gibt es aktuell keine Aufzählungslisten. Dabei bietet fast jeder Artikel Stellen, an denen eine Liste die Information verdichtet und verständlicher macht.'; $advice['example'] = 'Statt: „Du brauchst dafür Geduld, die richtige Ausrüstung, etwas Erfahrung und außerdem solltest du gut vorbereitet sein."' . " " . 'Besser als Liste:' . " " . '• Geduld und Ausdauer' . " " . '• Die passende Ausrüstung' . " " . '• Grundlegende Erfahrung' . " " . '• Gute Vorbereitung'; $advice['checklist'] = array( 'Suche nach Stellen mit 3+ Elementen in einem Satz – das sind ideale Listen', 'Verwende ul für ungeordnete Listen (Tipps, Vorteile, Merkmale)', 'Verwende ol für Schritt-für-Schritt-Anleitungen mit Reihenfolge', 'Achte darauf: jeder Listenpunkt beginnt mit einem aktiven Verb oder einem Substantiv', ); $advice['tip'] = 'Google liebt Listen mit 4–8 Punkten für Featured Snippets. Formuliere die Listenüberschrift als Frage: „Was brauchst du für...?" oder „Wie funktioniert...?"'; } // Absätze elseif ( strpos( $issue['msg'], 'Absatz' ) !== false ) { $para_count = count( array_filter( preg_split( '/ {2,}/', $text ) ) ); $advice['why'] = 'Große Textblöcke ohne Absätze wirken wie eine Wand aus Text. Leser brechen hier ab – besonders auf dem Smartphone. Kurze, klar getrennte Absätze erhöhen die Lesbarkeit und signalisieren Struktur.'; $advice['situation'] = 'Dein Artikel „' . $title_short . '" hat aktuell ca. ' . $para_count . ' Absätze. Für einen gut lesbaren Text empfehlen sich mindestens 6–8 Absätze, die jeweils 2–4 Sätze enthalten.'; $advice['example'] = 'Grundregel: Ein Gedanke = ein Absatz.' . " " . 'Faustregel für Absatzlänge: Wenn ein Absatz mehr als 4 Sätze hat, prüfe ob du ihn teilen kannst. Auf dem Smartphone sollte kein Absatz länger als einen Bildschirm sein.'; $advice['checklist'] = array( 'Teile lange Textblöcke nach jedem abgeschlossenen Gedanken auf', 'Jeder Absatz sollte eine einzige Kernaussage haben', 'Nutze kurze „Brücken-Sätze" zwischen Absätzen für Lesefluss', 'Prüfe die mobile Ansicht: Sieht der Text auf dem Handy noch lesbar aus?', ); $advice['tip'] = 'Schreib deinen ersten Satz jedes Absatzes so, dass jemand der nur die Erstzeilen liest trotzdem den Kerninhalt versteht.'; } // E-E-A-T: Berufsbezeichnung elseif ( strpos( $issue['msg'], 'Berufsbezeichnung' ) !== false ) { $advice['why'] = 'Google bewertet seit dem E-E-A-T Update besonders stark, wer einen Artikel geschrieben hat. Bei Gesundheits-, Rechts- oder Finanzthemen (YMYL) ist die Autorenautorität ein direkter Rankingfaktor. Eine fehlende Berufsbezeichnung signalisiert: „Wir wissen nicht wer das ist."'; $advice['situation'] = 'Der Autor dieses Artikels hat noch keine Berufsbezeichnung im Profil hinterlegt. Google sieht dann nur einen Namen – ohne Kontext über die Expertise hinter dem Inhalt von „' . $title_short . '".'; $advice['example'] = 'Statt keiner Angabe:' . " " . '→ „Staatlich geprüfte Hebamme" (konkret + Qualifikation)' . " " . '→ „Ernährungsberaterin (DGE)" (konkret + Zertifizierung)' . " " . '→ „Fachanwalt für Arbeitsrecht" (konkret + Fachgebiet)' . " " . 'Vermeide vage Bezeichnungen wie „Experte", „Berater" oder „Spezialist" ohne Kontext.'; $advice['checklist'] = array( 'Gehe zu Benutzer → Profil → Experten-Profil (Bereich weiter unten)', 'Trage deine genaue Berufsbezeichnung ein – so wie sie auf deinem Zeugnis/Diplom steht', 'Optional: Ergänze Zertifizierungen oder Fachgebiete in Klammern', 'Speichern – das Plugin trägt die Bezeichnung automatisch in das Schema.org Person-Markup ein', ); $advice['tip'] = 'Google prüft die Berufsbezeichnung im Kontext anderer Signale. Je konsistenter sie mit deiner Website, deinen Social-Media-Profilen und deiner Biografie übereinstimmt, desto stärker der Vertrauenseffekt.'; } // E-E-A-T: Fachbiografie elseif ( strpos( $issue['msg'], 'Fachbiografie' ) !== false || strpos( $issue['msg'], 'biografie' ) !== false ) { $expertise = get_user_meta( $post->post_author, self::USER_META_EXPERTISE, true ); $bio_len = mb_strlen( $expertise ); $advice['why'] = 'Die Autorenbiografie ist das stärkste E-E-A-T-Signal nach der Berufsbezeichnung. Sie erklärt nicht nur wer jemand ist, sondern warum er oder sie die Autorität hat, über dieses Thema zu schreiben. Google nutzt Biografie-Signale um Inhalte als vertrauenswürdig einzustufen.'; $advice['situation'] = $bio_len > 0 ? 'Die aktuelle Biografie ist mit ' . $bio_len . ' Zeichen noch zu kurz. Google und Leser erwarten mindestens 80–150 Zeichen – eine kurze, substanzielle Aussage über die Expertise.' : 'Für den Autor dieses Artikels ist noch keine Fachbiografie hinterlegt. Das fehlt komplett im Schema.org-Markup.'; $advice['example'] = 'Zu kurz: „Ich bin Hebamme und helfe Familien."' . " " . 'Besser: „Staatlich geprüfte Hebamme mit 12 Jahren Erfahrung in München. Schwerpunkt: natürliche Geburtsvorbereitung und Wochenbettbegleitung. Autorin von zwei Fachbüchern zum Thema Stillberatung."'; $advice['checklist'] = array( 'Benutzer → Profil → Experten-Profil → Fachbiografie ausfüllen', 'Nenne: Qualifikation + Jahre Erfahrung + konkretes Fachgebiet', 'Optional: Publikationen, Auszeichnungen, Zusatzqualifikationen', 'Ziel: 100–200 Zeichen – prägnant und substanziell', ); $advice['tip'] = 'Schreib die Biografie in der dritten Person – das wirkt professioneller und eignet sich besser für automatische Schema-Ausgabe.'; } // E-E-A-T: SameAs elseif ( strpos( $issue['msg'], 'SameAs' ) !== false ) { $advice['why'] = 'SameAs-Links verbinden dein WordPress-Profil mit externen Autoritätsquellen. Google nutzt diese Verbindungen um zu verifizieren, dass der Autor eine reale Fachperson ist – nicht eine anonyme Stimme im Internet. LinkedIn, Instagram und Wikipedia-Einträge sind dabei besonders wertvoll.'; $advice['situation'] = 'Für den Autor von „' . $title_short . '" sind noch keine externen Profillinks hinterlegt. Ohne SameAs-Links fehlt Google die Möglichkeit, die Autorität des Autors extern zu verifizieren.'; $advice['example'] = 'Empfohlene SameAs-Links in Reihenfolge der SEO-Wirkung:' . " " . '1. LinkedIn-Profil (stärkstes Autorität-Signal)' . " " . '2. Instagram mit fachlichem Content' . " " . '3. XING-Profil' . " " . '4. Google Scholar / ResearchGate (für Wissenschaftler)' . " " . '5. Eigene Praxis-/Unternehmenswebsite'; $advice['checklist'] = array( 'Benutzer → Profil → Experten-Profil → SameAs-Links', 'Eine URL pro Zeile eintragen', 'Priorität: LinkedIn + Instagram + eigene Website = 3 Links = maximaler Score-Bonus', 'Stelle sicher dass der Name auf den Profilen identisch ist', ); $advice['tip'] = 'Konsistenz ist entscheidend: Der Name, die Berufsbezeichnung und das Profilbild sollten auf allen verlinkten Profilen übereinstimmen. Google vergleicht diese Signale aktiv.'; } // E-E-A-T: Externe Quellen elseif ( strpos( $issue['msg'], 'extern' ) !== false || strpos( $issue['msg'], 'Quell' ) !== false ) { $advice['why'] = 'Externe Links zu seriösen Quellen signalisieren Google: „Dieser Autor hat recherchiert und belegt seine Aussagen." Das erhöht die Glaubwürdigkeit des Inhalts – besonders bei medizinischen, rechtlichen oder wissenschaftlichen Themen. Fehlende Quellenangaben wirken bei Fachthemen wie unbelegte Behauptungen.'; $advice['situation'] = 'In „' . $title_short . '" gibt es noch keine externen Quellenlinks. Das ist besonders bei einem Fachthema wie diesem eine verpasste Chance für Autorität und Vertrauen.'; $advice['example'] = 'Gute externe Quellen je nach Thema:' . " " . '• Medizin: PubMed, WHO, Robert Koch Institut, Bundeszentrale für gesundheitliche Aufklärung' . " " . '• Recht: Gesetze im Internet (gesetze-im-internet.de), Bundesjustizamt' . " " . '• Statistiken: Destatis, Eurostat, Statista' . " " . '• Allgemein: Wikipedia als Kontext-Link ist akzeptabel, aber nicht als Hauptquelle'; $advice['checklist'] = array( 'Identifiziere 1–3 konkrete Behauptungen in deinem Artikel, die eine Quelle verdienen', 'Verlinke direkt auf die Quelle mit beschreibendem Ankertext (nicht „hier klicken")', 'Nutze das title-Attribut für Quellenangaben: <a href="..." title="Studie: ...">', 'Externe Links können mit target="_blank" rel="noopener noreferrer" in neuem Tab öffnen', ); $advice['tip'] = 'Externe Links schaden deiner SEO nicht – sie stärken sie. Google interpretiert ausgehende Links zu Autoritätsquellen als positives Qualitätssignal, nicht als „Abfluss von Link-Juice".'; } // Technical: Featured Image elseif ( strpos( $issue['msg'], 'Beitragsbild' ) !== false ) { $advice['why'] = 'Das Featured Image ist das erste was Nutzer sehen – in Google-Suchergebnissen, in Google Discover, beim Teilen in Social Media. Artikel ohne Featured Image werden in Google Discover praktisch nie ausgespielt und haben deutlich niedrigere Klickraten in den Suchergebnissen.'; $advice['situation'] = 'Der Artikel „' . $title_short . '" hat noch kein Beitragsbild. Das bedeutet: kein Thumbnail in Google Discover, kein Preview-Bild beim Teilen auf LinkedIn oder WhatsApp, schwächere Klickrate in den Suchergebnissen.'; $advice['example'] = 'Was ein gutes Featured Image ausmacht:' . " " . '• Mindestgröße: 1200 × 630 Pixel (optimal für Social Sharing)' . " " . '• Aussagekräftig: Das Bild erklärt auf einen Blick worum es geht' . " " . '• Kein Stock-Foto-Klischee: Authentische Bilder performen besser' . " " . '• Alt-Text nicht vergessen: Beschreibt das Bild für Screenreader und Google'; $advice['checklist'] = array( 'Beitrag öffnen → rechts „Beitragsbild" → Bild hochladen oder auswählen', 'Bildgröße: mindestens 1200 × 630 Pixel', 'Dateiname vor dem Upload sprechend benennen: z.B. „hebamme-geburtvorbereitung.jpg" statt „IMG_4521.jpg"', 'Alt-Text beim Hochladen ausfüllen – beschreibe was auf dem Bild zu sehen ist', ); $advice['tip'] = 'Wenn du kein eigenes Bild hast: Unsplash.com und Pexels.com bieten kostenlose, hochwertige Fotos. Suche nach deinem Hauptthema und wähle ein authentisch wirkendes Bild.'; } // Technical: Alt-Texte elseif ( strpos( $issue['msg'], 'Alt-Text' ) !== false ) { preg_match_all( '/<img[^>]+>/i', $content, $imgs ); $img_count = count( $imgs[0] ); $advice['why'] = 'Alt-Texte erfüllen zwei Funktionen: Sie machen Bilder für Suchmaschinen lesbar (Google-Bildersuche!) und für sehbehinderte Nutzer zugänglich (Screenreader). Fehlende Alt-Texte sind verschenkte SEO-Punkte und ein Barrierefreiheits-Problem.'; $advice['situation'] = 'In „' . $title_short . '" ' . ( $img_count > 0 ? 'fehlen bei ' . $img_count . ' Bild(ern) die Alt-Texte.' : 'sind keine Bilder vorhanden.' ); $advice['example'] = 'Schlechter Alt-Text: „Bild1.jpg" oder „Foto"' . " " . 'Guter Alt-Text: „Hebamme erklärt werdender Mutter die Atemlibung zur Geburtsvorbereitung"' . " " . 'Regel: Beschreibe was auf dem Bild passiert – nicht was du dir dabei gedacht hast.'; $advice['checklist'] = array( 'Klicke auf jedes Bild im Editor → „Bild bearbeiten" → Alt-Text eintragen', 'Beschreibe konkret was auf dem Bild zu sehen ist (Person, Aktion, Kontext)', 'Baue das Hauptkeyword ein wenn es natürlich passt – nicht erzwungen', 'Maximal 125 Zeichen – Screenreader lesen nicht mehr vor', ); $advice['tip'] = 'Stelle dir vor, du beschreibst das Bild am Telefon jemandem der es nicht sehen kann. Genau das ist ein guter Alt-Text.'; } // Technical: Meta-Description elseif ( strpos( $issue['msg'], 'Meta-Description' ) !== false || strpos( $issue['msg'], 'Meta Description' ) !== false ) { $meta_desc = get_post_meta( $post->ID, '_yoast_wpseo_metadesc', true ); if ( empty( $meta_desc ) ) $meta_desc = get_post_meta( $post->ID, 'rank_math_description', true ); if ( empty( $meta_desc ) ) $meta_desc = get_post_meta( $post->ID, '_aije_metadesc', true ); $meta_len = mb_strlen( $meta_desc ); $advice['why'] = 'Die Meta-Description erscheint unter deinem Seitentitel in den Google-Suchergebnissen. Sie entscheidet ob jemand klickt oder nicht. Eine fehlende oder zu kurze Meta-Description wird oft durch Google automatisch ersetzt – und das Ergebnis ist meist schlechter als ein manuell geschriebener Text.'; $advice['situation'] = empty( $meta_desc ) ? 'Für „' . $title_short . '" ist noch keine Meta-Description hinterlegt. Google generiert dann automatisch einen Auszug – meistens den ersten Satz des Artikels, was selten optimal ist.' : 'Die Meta-Description von „' . $title_short . '" ist mit ' . $meta_len . ' Zeichen ' . ( $meta_len < 120 ? 'zu kurz (unter 120 Zeichen).' : 'zu lang (über 160 Zeichen, wird abgeschnitten).' ); $advice['example'] = 'Für einen Artikel über „' . $title_short . '" könnte eine gute Meta-Description so aussehen:' . " " . '„[Konkrete Zahl oder Aussage] + [Hauptnutzen für den Leser] + [Handlungsaufforderung]"' . " " . 'Beispiel: „In diesem Artikel erfährst du 5 erprobte Methoden zur Geburtsvorbereitung – von einer staatlich geprüften Hebamme. Jetzt lesen →"'; $advice['checklist'] = array( 'Öffne Yoast SEO oder RankMath weiter unten im Beitrag', 'Trage eine Meta-Description zwischen 120 und 160 Zeichen ein', 'Baue das Hauptkeyword früh ein (idealerweise in den ersten 70 Zeichen)', 'Formuliere einen konkreten Nutzen oder eine Handlungsaufforderung am Ende', ); $advice['tip'] = 'Schreib die Meta-Description wie eine Mini-Werbeanzeige: Was bekommt der Leser wenn er klickt? Warum sollte er jetzt klicken und nicht auf das andere Ergebnis?'; } // Technical: Titel elseif ( strpos( $issue['msg'], 'Titel' ) !== false ) { $tlen = mb_strlen( get_the_title( $post ) ); $advice['why'] = 'Der Titel-Tag ist der wichtigste SEO-Faktor einer Seite. Er erscheint als blauer Link in den Suchergebnissen. Ist er zu kurz, verschenkst du Keyword-Potenzial. Ist er zu lang, schneidet Google ihn ab – und der Klickanreiz geht verloren.'; $advice['situation'] = 'Der Titel „' . $title_short . '" hat ' . $tlen . ' Zeichen. ' . ( $tlen < 35 ? 'Das ist zu kurz – du vergibst wertvollen Platz für Keywords und Kaufargumente.' : 'Das überschreitet die empfohlene Länge von 65 Zeichen – Google schneidet ihn wahrscheinlich ab.' ); $advice['example'] = 'Schwach: „Geburtsvorbereitung" (zu kurz, kein Nutzen)' . " " . 'Besser: „Geburtsvorbereitung: 7 Methoden die wirklich helfen | Hebamme erklärt"' . " " . 'Gut funktionieren: Zahlen (7 Tipps), Fragen (Wie...?), Versprechen (So funktioniert...), Zielgruppe (Für Erstgebärende)'; $advice['checklist'] = array( 'Ziel: 50–65 Zeichen inkl. Leerzeichen', 'Baue das Hauptkeyword in die ersten 50 Zeichen ein', 'Füge einen Nutzenversprechen oder eine Zahl hinzu wenn sinnvoll', 'Prüfe mit einem Title-Tag-Vorschau-Tool (z.B. seobility.net/tools/serp-snippet-generator)', ); $advice['tip'] = 'Titel mit Zahlen ("5 Methoden", "7 Tipps") haben nachweislich höhere Klickraten. Fragestellungen ("Wie...", "Was ist...") sprechen Informations-Suchende direkt an.'; } // Linking: Interne Links elseif ( strpos( $issue['msg'], 'intern' ) !== false && $category === 'linking' ) { // Thematisch passende Beitraege per Keyword-Extraktion finden $related = $this->find_related_posts( $post, 6 ); $advice['why'] = 'Interne Links sind das Nervensystem deiner Website. Sie verteilen Autorität zwischen deinen Seiten, helfen Google deine Website-Struktur zu verstehen und halten Leser länger auf deiner Website. Artikel ohne interne Links sind wie Inseln – sie geben ihre SEO-Kraft nicht weiter.'; $advice['situation'] = 'Der Artikel „' . $title_short . '“ verlinkt noch auf keine anderen Seiten deiner Website. Das bedeutet: Leser, die mehr wissen wollen, verlassen deine Website. Und Google sieht keinen Kontext zu anderen Inhalten.'; if ( ! empty( $related ) ) { $advice['related_posts'] = $related; $advice['example'] = 'Basierend auf deinem Inhalt haben wir thematisch passende Artikel gefunden (siehe Vorschlaege unten). Gute Ankertexte:' . "\n\n" . '✓ „Mehr dazu: [Titel des Artikels]“' . "\n" . '✓ „Wie wir in [Titel] beschreiben...“' . "\n" . '✓ „Weitere Infos: [Titel des Artikels]“' . "\n\n" . '✗ Vermeide: „Hier klicken“ oder „Mehr erfahren“ ohne Kontext'; } else { $advice['example'] = 'Gute Ankertexte fuer interne Links:' . "\n\n" . '✓ „Mehr dazu in unserem Artikel über [verwandtes Thema]“' . "\n" . '✓ „Wie wir in [verlinktem Artikel] beschreiben...“' . "\n\n" . '✗ Vermeide: „Hier klicken“, „Mehr erfahren“ als alleinigen Ankertext'; } $advice['checklist'] = array( 'Wähle 2–3 der unten vorgeschlagenen Artikel aus', 'Füge die Links an thematisch passenden Stellen im Text ein', 'Ideal: Ein Link früh im Artikel, einer am Ende als weiterführende Empfehlung', 'Prüfe auch: Verlinken andere Artikel schon auf diesen hier? (Orphan-Content vermeiden)', ); $advice['tip'] = 'Die stärksten internen Links kommen von deinen Seiten mit dem meisten Traffic. Ergänze dort Links zu neuen oder schwächeren Seiten um deren Autorität zu stärken.'; } // Aktualität elseif ( strpos( $issue['msg'], 'aktualisiert' ) !== false ) { $months_ago = round( ( time() - strtotime( $post->post_modified ) ) / MONTH_IN_SECONDS ); $advice['why'] = 'Google bevorzugt aktuellen Content – besonders in Bereichen wo sich Informationen schnell ändern. Das „Freshness"-Signal fließt direkt in das Ranking ein. Ein Artikel der seit über einem Jahr nicht angefasst wurde, verliert schrittweise an Sichtbarkeit.'; $advice['situation'] = 'Der Artikel „' . $title_short . '" wurde zuletzt vor ca. ' . $months_ago . ' Monaten aktualisiert. In vielen Themenbereichen haben sich in dieser Zeit Fakten, Zahlen oder Empfehlungen geändert.'; $advice['example'] = 'Was bei einer Aktualisierung wirklich zählt:' . " " . '• Veraltete Zahlen, Statistiken oder Gesetze aktualisieren' . " " . '• Neue wissenschaftliche Erkenntnisse einarbeiten' . " " . '• Veraltete Links auf neue Quellen umleiten' . " " . '• Ein kurzes Update-Datum am Anfang: „Zuletzt aktualisiert: März 2025"'; $advice['checklist'] = array( 'Lies den Artikel durch: Gibt es veraltete Informationen, Zahlen oder Empfehlungen?', 'Aktualisiere mindestens 1-2 Absätze mit neuen Erkenntnissen', 'Ergänze ein „Zuletzt aktualisiert"-Datum am Anfang des Artikels', 'Speichere – WordPress aktualisiert den „post_modified" Timestamp automatisch', ); $advice['tip'] = 'Du musst nicht alles neu schreiben. Manchmal reicht es, einen Abschnitt zu ergänzen, eine neue Studie zu verlinken oder die Einleitung zu modernisieren. Google erkennt Änderungen ab ca. 10% des Inhalts.'; } // Fallback für unbekannte Issue-Typen else { $advice['why'] = 'Dieser Faktor hat direkten Einfluss auf die Sichtbarkeit und Bewertung deines Inhalts durch Suchmaschinen und KI-Systeme.'; $advice['situation'] = $issue['msg']; $advice['example'] = $issue['action'] ?? ''; $advice['checklist'] = array( $issue['action'] ?? 'Prüfe die Details und nimm die empfohlene Änderung vor.' ); $advice['tip'] = 'Kleine Verbesserungen summieren sich. Jede behobene Issue erhöht deinen AI Readiness Score.'; } return $advice; } private function calculate_ai_analysis( $post ) { $content = $post->post_content; $text = wp_strip_all_tags( $content ); $word_count = str_word_count( $text ); $issues = array(); // ── Hilfsdaten ──────────────────────────────────────────────────────── preg_match_all( '/<h2[^>]*>/i', $content, $h2 ); preg_match_all( '/<h3[^>]*>/i', $content, $h3 ); $h2_count = count( $h2[0] ); $h3_count = count( $h3[0] ); // Absätze direkt aus dem HTML zählen – zuverlässiger als Newline-Split auf reinem Text, // weil wp_strip_all_tags() Zeilenumbrüche zwischen <p>-Tags entfernen kann. preg_match_all( '/<p[^>]*>(.*?)<\/p>/si', $content, $p_matches ); $html_paras = array_filter( array_map( 'strip_tags', $p_matches[1] ), function( $p ) { return strlen( trim( $p ) ) > 20; } ); $para_count = count( $html_paras ); // Fallback: falls kein <p>-Markup vorhanden (z.B. reiner Text-Content), // auf Zeilenumbrüche zurückfallen if ( $para_count === 0 ) { $fallback = array_filter( array_map( 'trim', preg_split( '/\n{2,}/', $text ) ), function( $p ) { return strlen( $p ) > 20; } ); $para_count = count( $fallback ); } preg_match_all( '/<a[^>]+href=["\']?([^"\'> ]+)["\']?/i', $content, $link_matches ); $internal_links = 0; $external_links = 0; // Schema-unabhängig vergleichen: http und https beide als intern erkennen $home_norm = preg_replace( '/^https?:/', '', home_url() ); // z.B. '//example.com' foreach ( $link_matches[1] as $href ) { $href_norm = preg_replace( '/^https?:/', '', $href ); if ( strpos( $href_norm, $home_norm ) !== false || strpos( $href, '/' ) === 0 ) { $internal_links++; } elseif ( strpos( $href, 'http' ) === 0 ) { $external_links++; } } $has_list = ( strpos( $content, '<ul' ) !== false || strpos( $content, '<ol' ) !== false ); $has_faq = (bool) preg_match( '/\b(faq|häufig|frequently|fragen)\b/i', $text ); $sentences = preg_split( '/[.!?]+/', $text, -1, PREG_SPLIT_NO_EMPTY ); $sentences = array_filter( $sentences, function( $s ) { return str_word_count( trim( $s ) ) > 2; } ); $sentence_count = count( $sentences ); $avg_words_per_sentence = $sentence_count > 0 ? round( $word_count / $sentence_count ) : 0; preg_match_all( '/<img[^>]+>/i', $content, $img_matches ); $img_count = count( $img_matches[0] ); $imgs_with_alt = 0; foreach ( $img_matches[0] as $img ) { if ( preg_match( '/alt=["\'][^"\']+["\']/i', $img ) ) { $imgs_with_alt++; } } $has_featured_image = has_post_thumbnail( $post->ID ); $meta_desc = get_post_meta( $post->ID, '_yoast_wpseo_metadesc', true ); if ( empty( $meta_desc ) ) { $meta_desc = get_post_meta( $post->ID, 'rank_math_description', true ); } if ( empty( $meta_desc ) ) { $meta_desc = get_post_meta( $post->ID, '_aioseo_description', true ); } if ( empty( $meta_desc ) ) { $meta_desc = get_post_meta( $post->ID, '_aije_metadesc', true ); } // eigene Meta-Box $has_meta_desc = ! empty( $meta_desc ); $job_title = get_user_meta( $post->post_author, self::USER_META_JOB_TITLE, true ); $expertise = get_user_meta( $post->post_author, self::USER_META_EXPERTISE, true ); $expert_url = get_user_meta( $post->post_author, self::USER_META_WEBSITE, true ); $same_as_raw = get_user_meta( $post->post_author, self::USER_META_SAME_AS, true ); $same_as_count = 0; if ( ! empty( $same_as_raw ) ) { foreach ( preg_split( "/\r\n|\n|\r/", $same_as_raw ) as $line ) { $line = trim( $line ); if ( $line !== '' && substr( $line, 0, 1 ) !== '#' ) { $same_as_count++; } } } $is_recent = strtotime( $post->post_modified ) > strtotime( '-12 months' ); $title = $post->post_title; $title_len = mb_strlen( $title ); $title_has_nr = (bool) preg_match( '/\d/', $title ); $title_has_q = ( strpos( $title, '?' ) !== false ); // ── Kategorie 1: Content-Qualität (35 %) ───────────────────────────── $c_score = 0; if ( $word_count >= 1500 ) { $c_score += 40; } elseif ( $word_count >= 1000 ) { $c_score += 32; } elseif ( $word_count >= 700 ) { $c_score += 22; } elseif ( $word_count >= 400 ) { $c_score += 12; } elseif ( $word_count >= 200 ) { $c_score += 5; } else { $issues[] = array( 'msg' => 'Inhalt zu kurz (' . $word_count . ' Wörter).', 'action' => 'Mindestens 700 Wörter anstreben; 1.000+ für konkurrenzfähige Themen.', 'points' => 30, 'category' => 'content', 'severity' => 'high' ); } if ( $word_count > 0 && $word_count < 700 ) { $issues[] = array( 'msg' => 'Artikel etwas kurz (' . $word_count . ' Wörter).', 'action' => 'Erweitere auf mindestens 700 Wörter für bessere Sichtbarkeit.', 'points' => 18, 'category' => 'content', 'severity' => 'medium' ); } if ( $h2_count >= 4 ) { $c_score += 25; } elseif ( $h2_count >= 2 ) { $c_score += 18; } elseif ( $h2_count === 1 ) { $c_score += 10; } else { $issues[] = array( 'msg' => 'Keine H2-Zwischenüberschriften.', 'action' => 'Strukturiere den Text mit mindestens 3 H2-Überschriften.', 'points' => 20, 'category' => 'content', 'severity' => 'high' ); } if ( $h3_count >= 2 ) { $c_score += 5; } if ( $avg_words_per_sentence >= 10 && $avg_words_per_sentence <= 20 ) { $c_score += 15; } elseif ( $avg_words_per_sentence > 20 && $avg_words_per_sentence <= 28 ) { $c_score += 8; $issues[] = array( 'msg' => 'Satzlänge leicht erhöht (' . $avg_words_per_sentence . ' Wörter/Satz).', 'action' => 'Lange Sätze aufteilen; Zielwert: 12–18 Wörter.', 'points' => 7, 'category' => 'content', 'severity' => 'low' ); } elseif ( $avg_words_per_sentence > 28 ) { $issues[] = array( 'msg' => 'Sätze sehr lang (' . $avg_words_per_sentence . ' Wörter/Satz).', 'action' => 'Text in kürzere Sätze umstrukturieren.', 'points' => 15, 'category' => 'content', 'severity' => 'medium' ); } elseif ( $avg_words_per_sentence > 0 ) { $c_score += 10; } if ( $has_list ) { $c_score += 7; } else { $issues[] = array( 'msg' => 'Keine Aufzählungslisten.', 'action' => 'Mindestens eine ul/ol-Liste einfügen.', 'points' => 5, 'category' => 'content', 'severity' => 'low' ); } if ( $has_faq ) { $c_score += 3; } if ( $para_count >= 6 ) { $c_score += 10; } elseif ( $para_count >= 3 ) { $c_score += 6; } else { $issues[] = array( 'msg' => 'Zu wenige Absätze (' . $para_count . ').', 'action' => 'Text in mindestens 5–6 Absätze gliedern.', 'points' => 8, 'category' => 'content', 'severity' => 'medium' ); } $c_score = min( 100, $c_score ); // ── Kategorie 2: E-E-A-T / Autorität (30 %) ────────────────────────── $e_score = 0; if ( ! empty( $job_title ) ) { $e_score += 20; } else { $issues[] = array( 'msg' => 'Keine Berufsbezeichnung im Autorenprofil.', 'action' => 'Unter Benutzer → Profil → "Experten-Profil" Berufsbezeichnung hinterlegen.', 'points' => 20, 'category' => 'eeat', 'severity' => 'high' ); } if ( ! empty( $expertise ) && mb_strlen( $expertise ) >= 80 ) { $e_score += 20; } elseif ( ! empty( $expertise ) ) { $e_score += 10; $issues[] = array( 'msg' => 'Fachbiografie sehr kurz (' . mb_strlen( $expertise ) . ' Zeichen).', 'action' => 'Biografie auf mindestens 80 Zeichen ausbauen.', 'points' => 10, 'category' => 'eeat', 'severity' => 'medium' ); } else { $issues[] = array( 'msg' => 'Keine Fachbiografie im Autorenprofil.', 'action' => 'Kurze Fachbiografie im Benutzerprofil eintragen.', 'points' => 20, 'category' => 'eeat', 'severity' => 'high' ); } if ( $same_as_count >= 3 ) { $e_score += 25; } elseif ( $same_as_count === 2 ) { $e_score += 18; } elseif ( $same_as_count === 1 ) { $e_score += 10; } else { $issues[] = array( 'msg' => 'Keine SameAs-Links (Social Media / Profilseiten).', 'action' => 'Instagram, LinkedIn o.ä. im Benutzerprofil hinterlegen.', 'points' => 25, 'category' => 'eeat', 'severity' => 'high' ); } if ( ! empty( $expert_url ) ) { $e_score += 15; } else { $issues[] = array( 'msg' => 'Keine Experten-/Praxiswebsite hinterlegt.', 'action' => 'URL der eigenen Website/Praxis im Benutzerprofil eintragen.', 'points' => 15, 'category' => 'eeat', 'severity' => 'medium' ); } if ( $external_links >= 3 ) { $e_score += 20; } elseif ( $external_links === 2 ) { $e_score += 15; } elseif ( $external_links === 1 ) { $e_score += 8; } else { $issues[] = array( 'msg' => 'Keine externen Quellen verlinkt.', 'action' => 'Verlinke 1–3 vertrauenswürdige Quellen (Studien, Behörden, Fachportale).', 'points' => 15, 'category' => 'eeat', 'severity' => 'medium' ); } $e_score = min( 100, $e_score ); // ── Kategorie 3: Technische SEO (20 %) ─────────────────────────────── $t_score = 0; if ( $has_featured_image ) { $t_score += 30; } else { $issues[] = array( 'msg' => 'Kein Beitragsbild gesetzt.', 'action' => 'Featured Image setzen – wichtig für Social Shares und Google Discover.', 'points' => 30, 'category' => 'technical', 'severity' => 'high' ); } if ( $img_count === 0 ) { $issues[] = array( 'msg' => 'Keine Bilder im Artikelinhalt.', 'action' => 'Mindestens 1–2 thematisch passende Bilder einfügen.', 'points' => 15, 'category' => 'technical', 'severity' => 'low' ); } elseif ( $imgs_with_alt === $img_count ) { $t_score += 25; } elseif ( $imgs_with_alt > 0 ) { $t_score += 12; $issues[] = array( 'msg' => ( $img_count - $imgs_with_alt ) . ' Bild(er) ohne Alt-Text.', 'action' => 'Jedem Bild einen beschreibenden Alt-Text hinzufügen.', 'points' => 13, 'category' => 'technical', 'severity' => 'medium' ); } else { $issues[] = array( 'msg' => 'Alle ' . $img_count . ' Bilder ohne Alt-Text.', 'action' => 'Alt-Texte für alle Bilder ergänzen – wichtig für Barrierefreiheit & SEO.', 'points' => 25, 'category' => 'technical', 'severity' => 'high' ); } if ( $has_meta_desc ) { $meta_len = mb_strlen( $meta_desc ); if ( $meta_len >= 120 && $meta_len <= 160 ) { $t_score += 25; } elseif ( $meta_len >= 70 ) { $t_score += 15; $issues[] = array( 'msg' => 'Meta-Description Länge nicht optimal (' . $meta_len . ' Zeichen).', 'action' => 'Zielbereich: 120–160 Zeichen.', 'points' => 10, 'category' => 'technical', 'severity' => 'low' ); } else { $t_score += 8; $issues[] = array( 'msg' => 'Meta-Description zu kurz (' . $meta_len . ' Zeichen).', 'action' => 'Auf 120–160 Zeichen ausbauen.', 'points' => 17, 'category' => 'technical', 'severity' => 'medium' ); } } else { $issues[] = array( 'msg' => 'Keine Meta-Description gefunden.', 'action' => 'Meta-Description im WP Rankify Meta-Generator oder in Yoast/RankMath/AIOSEO hinterlegen.', 'points' => 25, 'category' => 'technical', 'severity' => 'high' ); } if ( $title_len >= 50 && $title_len <= 65 ) { $t_score += 20; } elseif ( $title_len >= 35 ) { $t_score += 12; if ( $title_len > 65 ) { $issues[] = array( 'msg' => 'Titel zu lang (' . $title_len . ' Zeichen).', 'action' => 'Auf max. 65 Zeichen kürzen.', 'points' => 8, 'category' => 'technical', 'severity' => 'low' ); } } else { $issues[] = array( 'msg' => 'Titel sehr kurz (' . $title_len . ' Zeichen).', 'action' => 'Titel auf 50–65 Zeichen ausbauen.', 'points' => 15, 'category' => 'technical', 'severity' => 'medium' ); } if ( $title_has_nr || $title_has_q ) { $t_score += 5; } $t_score = min( 100, $t_score ); // ── Kategorie 4: Interne Vernetzung (15 %) ─────────────────────────── $l_score = 0; if ( $internal_links >= 4 ) { $l_score += 60; } elseif ( $internal_links === 3 ) { $l_score += 50; } elseif ( $internal_links === 2 ) { $l_score += 38; } elseif ( $internal_links === 1 ) { $l_score += 22; } else { $issues[] = array( 'msg' => 'Keine internen Links.', 'action' => 'Verlinke 2–4 thematisch verwandte eigene Artikel.', 'points' => 50, 'category' => 'linking', 'severity' => 'high' ); } if ( $is_recent ) { $l_score += 40; } else { $age_months = round( ( time() - strtotime( $post->post_modified ) ) / MONTH_IN_SECONDS ); $issues[] = array( 'msg' => 'Artikel seit ' . $age_months . ' Monaten nicht aktualisiert.', 'action' => 'Inhalte, Zahlen und Links prüfen und aktualisieren.', 'points' => 35, 'category' => 'linking', 'severity' => 'medium' ); } $l_score = min( 100, $l_score ); // ── Gewichteter Gesamt-Score (aus Einstellungen / Preset) ─────────────── $weights = self::get_score_weights( $this->get_opts() ); $total = (int) round( ( $c_score * $weights['content'] / 100 ) + ( $e_score * $weights['eeat'] / 100 ) + ( $t_score * $weights['technical'] / 100 ) + ( $l_score * $weights['linking'] / 100 ) ); // Issues sortieren: high → medium → low, dann points desc $sev_order = array( 'high' => 0, 'medium' => 1, 'low' => 2 ); usort( $issues, function( $a, $b ) use ( $sev_order ) { $s = $sev_order[ $a['severity'] ] <=> $sev_order[ $b['severity'] ]; return $s !== 0 ? $s : ( $b['points'] <=> $a['points'] ); }); return array( 'score' => $total, 'categories' => array( 'content' => array( 'score' => $c_score, 'label' => 'Content-Qualität', 'weight' => $weights['content'] ), 'eeat' => array( 'score' => $e_score, 'label' => 'E-E-A-T / Autorität', 'weight' => $weights['eeat'] ), 'technical' => array( 'score' => $t_score, 'label' => 'Technische SEO', 'weight' => $weights['technical'] ), 'linking' => array( 'score' => $l_score, 'label' => 'Interne Vernetzung', 'weight' => $weights['linking'] ), ), 'issues' => $issues, ); } public function register_settings() { register_setting(self::OPT_KEY, self::OPT_KEY, array($this, 'sanitize_settings')); register_setting('aije_ai_settings', 'aije_openai_api_key'); } /** * Output-Buffer starten um Header-Konflikte durch andere Plugins zu vermeiden. * Wird bei allen admin_post und options-save Aktionen ausgeführt. */ public function start_output_buffer() { if ( ! ob_get_level() ) { ob_start(); } } public function sanitize_settings($in) { $d = self::defaults(); $o = array(); $o['enabled_post_types'] = isset($in['enabled_post_types']) ? array_values(array_map('sanitize_key', (array) $in['enabled_post_types'])) : $d['enabled_post_types']; $o['enable_schema'] = !empty($in['enable_schema']) ? 1 : 0; $o['minify'] = !empty($in['minify']) ? 1 : 0; $o['acf_include'] = !empty($in['acf_include']) ? 1 : 0; $o['cron_enabled'] = !empty($in['cron_enabled']) ? 1 : 0; $o['cron_frequency']= isset($in['cron_frequency']) ? sanitize_key($in['cron_frequency']) : 'daily'; $o['ping_enabled'] = !empty($in['ping_enabled']) ? 1 : 0; $o['ping_urls'] = isset($in['ping_urls']) ? (string) $in['ping_urls'] : $d['ping_urls']; $events = array(); if (!empty($in['ping_events']) && is_array($in['ping_events'])) { foreach ($in['ping_events'] as $e) { $e = sanitize_key($e); if (in_array($e, array('publish', 'update'), true)) { $events[] = $e; } } } $o['ping_events'] = array_values(array_unique($events)); $o['ping_secret'] = isset($in['ping_secret']) ? (string) $in['ping_secret'] : ''; $o['ping_timeout'] = isset($in['ping_timeout']) ? max(2, (int) $in['ping_timeout']) : 5; $o['ping_retry'] = !empty($in['ping_retry']) ? 1 : 0; $o['openai_api_key'] = isset($in['openai_api_key']) ? sanitize_text_field($in['openai_api_key']) : ''; $o['openai_prompt'] = isset($in['openai_prompt']) ? trim($in['openai_prompt']) : $d['openai_prompt']; // Score-Gewichtung $valid_presets = array( 'ymyl', 'standard', 'ecommerce', 'custom' ); $o['score_preset'] = ( isset( $in['score_preset'] ) && in_array( $in['score_preset'], $valid_presets, true ) ) ? $in['score_preset'] : 'standard'; $o['score_weight_content'] = isset( $in['score_weight_content'] ) ? max( 5, (int) $in['score_weight_content'] ) : 35; $o['score_weight_eeat'] = isset( $in['score_weight_eeat'] ) ? max( 5, (int) $in['score_weight_eeat'] ) : 30; $o['score_weight_technical'] = isset( $in['score_weight_technical'] ) ? max( 5, (int) $in['score_weight_technical'] ) : 20; $o['score_weight_linking'] = isset( $in['score_weight_linking'] ) ? max( 5, (int) $in['score_weight_linking'] ) : 15; $valid_seo = array( 'auto', 'yoast', 'rankmath', 'aioseo', 'own' ); $old_opts = get_option( self::OPT_KEY, array() ); $old_seo = is_array($old_opts) && isset($old_opts['seo_plugin']) ? $old_opts['seo_plugin'] : 'auto'; $o['seo_plugin'] = ( isset($in['seo_plugin']) && in_array($in['seo_plugin'], $valid_seo, true) ) ? $in['seo_plugin'] : $old_seo; $o['remove_category_base'] = ! empty($in['remove_category_base']) ? 1 : 0; $this->maybe_reschedule_export($o['cron_enabled'], $o['cron_frequency']); $this->ensure_ping_cron(); // Tracking $o['gtm_id'] = sanitize_text_field( $in['gtm_id'] ?? '' ); $o['ga4_id'] = sanitize_text_field( $in['ga4_id'] ?? '' ); $o['gsc_verify'] = sanitize_text_field( $in['gsc_verify'] ?? '' ); $o['meta_pixel'] = sanitize_text_field( $in['meta_pixel'] ?? '' ); $o['gsc_client_id'] = sanitize_text_field( $in['gsc_client_id'] ?? '' ); $o['gsc_client_secret'] = sanitize_text_field( $in['gsc_client_secret'] ?? '' ); return $o; } /* ===== Admin UI ===== */ public function tools_menu() { add_management_page( esc_html__('WP Rankify', 'ai-json-extractor-auto-embed-lite'), esc_html__('WP Rankify', 'ai-json-extractor-auto-embed-lite'), 'manage_options', 'aije-lite', array($this, 'render_tools') ); } public function render_tools() { if (!current_user_can('manage_options')) { return; } $licensed = self::license_is_valid(); $tabs = array( 'dashboard' => esc_html__('Dashboard', 'ai-json-extractor-auto-embed-lite'), 'settings' => esc_html__('Einstellungen', 'ai-json-extractor-auto-embed-lite'), 'tracking' => esc_html__('Tracking & Analytics', 'ai-json-extractor-auto-embed-lite'), 'export' => esc_html__('Bulk Export', 'ai-json-extractor-auto-embed-lite'), 'automation' => esc_html__('Automation', 'ai-json-extractor-auto-embed-lite'), 'pinghub' => esc_html__('PingHub', 'ai-json-extractor-auto-embed-lite'), 'gsc' => esc_html__('Google Suche', 'ai-json-extractor-auto-embed-lite'), 'llms' => esc_html__('llms.txt', 'ai-json-extractor-auto-embed-lite'), 'license' => esc_html__('Lizenz', 'ai-json-extractor-auto-embed-lite'), ); $tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'dashboard'; echo '<div class="wrap" style="position:relative;">'; echo '<h1>' . esc_html__('WP Rankify', 'ai-json-extractor-auto-embed-lite') . ' <span style="font-size:13px;font-weight:400;color:#888;">(AI Auto Embed)</span></h1>'; echo '<h2 class="nav-tab-wrapper">'; foreach ($tabs as $k => $label) { $class = ($tab === $k) ? ' nav-tab nav-tab-active' : ' nav-tab'; $url = add_query_arg(array('page' => 'aije-lite', 'tab' => $k), admin_url('tools.php')); echo '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>'; } echo '</h2>'; // ── Bewertungs-Button ────────────────────────────────────────────────── echo '<p style="margin:8px 0 12px 0;">' . '<a href="https://wordpress.org/support/plugin/ai-json-extractor-auto-embed-lite/reviews/#new-post" target="_blank"' . ' style="display:inline-flex;align-items:center;gap:6px;background:#fff8f0;border:1px solid #f29400;' . 'border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;color:#f29400;text-decoration:none;">' . '⭐ WP Rankify bewerten' . '</a></p>'; if ($licensed) { echo '<div class="notice notice-success"><p>'; echo '<strong>' . esc_html__('Lizenz aktiv.', 'ai-json-extractor-auto-embed-lite') . '</strong>'; echo '</p></div>'; } switch ($tab) { case 'dashboard': $this->tab_dashboard(); break; case 'export': $this->tab_export(); break; case 'automation': $this->tab_automation(); break; case 'pinghub': $this->tab_pinghub(); break; case 'logs': case 'gsc': $this->tab_gsc(); break; case 'llms': $this->tab_llms(); break; case 'license': $this->tab_license(); break; case 'tracking': $this->tab_tracking(); break; case 'settings': default: $this->tab_settings(); break; } echo '</div>'; } private function tab_tracking() { if ( isset($_GET['settings-updated']) ) { $saved = get_option(self::OPT_KEY, array()); add_settings_error( 'aije_tracking', 'saved', 'GTM: ' . ($saved['gtm_id'] ?? 'leer') . ' | GA4: ' . ($saved['ga4_id'] ?? 'leer') . ' | Pixel: ' . ($saved['meta_pixel'] ?? 'leer'), 'success' ); } settings_errors( 'aije_tracking' ); $opts = $this->get_opts(); $gtm_id = $opts['gtm_id'] ?? ''; $ga4_id = $opts['ga4_id'] ?? ''; $gsc_verify = $opts['gsc_verify'] ?? ''; ?> <div style="max-width:680px;margin-top:16px;"> <form method="post" action="options.php"> <?php settings_fields(self::OPT_KEY); ?> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:16px;"> <h3 style="margin:0 0 4px 0;font-size:15px;font-weight:700;">🏷 Google Tag Manager</h3> <p style="margin:0 0 14px 0;font-size:13px;color:#6b7280;">Empfohlen wenn du mehrere Tracking-Tools verwaltest. GTM übernimmt dann auch GA4.</p> <label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;">Tag Manager ID</label> <input type="text" name="aije_pro_opts[gtm_id]" value="<?= esc_attr($gtm_id) ?>" placeholder="GTM-XXXXXXX" style="width:100%;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:monospace;" /> <p style="font-size:12px;color:#9ca3af;margin:6px 0 0 0;">Findest du unter tagmanager.google.com → dein Container → Container-ID</p> </div> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:16px;"> <h3 style="margin:0 0 4px 0;font-size:15px;font-weight:700;">📊 Google Analytics 4</h3> <p style="margin:0 0 14px 0;font-size:13px;color:#6b7280;">Direkte GA4-Einbindung ohne Tag Manager. Falls du GTM nutzt, lass dieses Feld leer.</p> <label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;">Mess-ID</label> <input type="text" name="aije_pro_opts[ga4_id]" value="<?= esc_attr($ga4_id) ?>" placeholder="G-XXXXXXXXXX" style="width:100%;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:monospace;" /> <p style="font-size:12px;color:#9ca3af;margin:6px 0 0 0;">Findest du unter analytics.google.com → Verwaltung → Datenströme → deine Website</p> </div> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px;"> <h3 style="margin:0 0 4px 0;font-size:15px;font-weight:700;">🔍 Google Search Console</h3> <p style="margin:0 0 14px 0;font-size:13px;color:#6b7280;">Verifiziere deine Website bei Google Search Console über einen Meta-Tag im <head>.</p> <label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;">Verifikations-Code</label> <input type="text" name="aije_pro_opts[gsc_verify]" value="<?= esc_attr($gsc_verify) ?>" placeholder="google-site-verification=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" style="width:100%;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:monospace;" /> <p style="font-size:12px;color:#9ca3af;margin:6px 0 0 0;">Findest du unter search.google.com/search-console → Eigenschaft hinzufügen → HTML-Tag → content="..."</p> <?php if ( $gsc_verify ) : ?> <p style="font-size:12px;color:#16a34a;margin:8px 0 0 0;">✓ Verifikations-Tag ist aktiv</p> <?php endif; ?> </div> <!-- Meta Pixel --> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px;"> <h3 style="margin:0 0 4px 0;font-size:15px;font-weight:700;">📺 Meta Pixel (Facebook)</h3> <p style="margin:0 0 14px 0;font-size:13px;color:#6b7280;">Für Facebook- und Instagram-Werbung. Pixel-Code wird automatisch auf allen Seiten eingebunden.</p> <label style="display:block;font-size:13px;font-weight:600;margin-bottom:5px;">Pixel-ID</label> <input type="text" name="aije_pro_opts[meta_pixel]" value="<?= esc_attr($opts['meta_pixel'] ?? '') ?>" placeholder="123456789012345" style="width:100%;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:monospace;" /> <p style="font-size:12px;color:#9ca3af;margin:6px 0 0 0;">Findest du unter business.facebook.com → Events Manager → Pixel → Pixel-ID</p> </div> <button type="submit" style="background:#f29400;color:#fff;border:none;padding:11px 28px;border-radius:7px;font-size:14px;font-weight:600;cursor:pointer;"> Einstellungen speichern </button> </form> <?php if ( $gtm_id || $ga4_id ) : ?> <div style="margin-top:20px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:16px 20px;font-size:13px;"> <strong style="color:#15803d;">✓ Aktives Tracking:</strong> <?php if ( $gtm_id ) : ?> <span style="margin-left:8px;background:#dcfce7;color:#15803d;padding:2px 10px;border-radius:4px;font-family:monospace;"><?= esc_html($gtm_id) ?></span> <?php endif; ?> <?php if ( $ga4_id ) : ?> <span style="margin-left:8px;background:#dcfce7;color:#15803d;padding:2px 10px;border-radius:4px;font-family:monospace;"><?= esc_html($ga4_id) ?></span> <?php endif; ?> </div> <?php endif; ?> </div> <?php } private function tab_settings() { $o = $this->get_opts(); $types = get_post_types(array('public' => true), 'objects'); $acf = function_exists('get_fields'); echo '<form method="post" action="options.php">'; settings_fields(self::OPT_KEY); echo '<table class="form-table">'; // SEO-Plugin – nur Anzeige, kein Dropdown $detected_seo = 'Keines erkannt – WP Rankify übernimmt Meta-Description'; if ( defined('WPSEO_VERSION') ) { $detected_seo = 'Yoast SEO ' . WPSEO_VERSION; } if ( defined('RANK_MATH_VERSION') ) { $detected_seo = 'RankMath ' . RANK_MATH_VERSION; } if ( defined('AIOSEO_VERSION') ) { $detected_seo = 'AIOSEO ' . AIOSEO_VERSION; } echo '<tr><th>SEO-Plugin</th><td>'; echo '<strong>' . esc_html($detected_seo) . '</strong>'; echo '<p class="description">WP Rankify erkennt automatisch welches SEO-Plugin installiert ist und arbeitet damit zusammen. Ist keines installiert, erscheint eine eigene SEO-Box im Editor.</p>'; echo '</td></tr>'; // Post Types echo '<tr><th>Aktive Post-Typen</th><td>'; foreach ($types as $t) { echo '<label style="display:inline-block;margin-right:14px;">'; echo '<input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[enabled_post_types][]" value="' . esc_attr($t->name) . '" ' . checked(in_array($t->name, $o['enabled_post_types'], true), true, false) . ' /> '; echo esc_html($t->labels->singular_name . ' (' . $t->name . ')'); echo '</label>'; } echo '</td></tr>'; // Schema echo '<tr><th>Schema.org (Article)</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[enable_schema]" value="1" ' . checked($o['enable_schema'], true, false) . ' /> '; echo 'application/ld+json (Article) zusätzlich ausgeben</label>'; echo '<p class="description">Das Experten-Profil (Person-Schema) wird immer automatisch ausgegeben, unabhängig von dieser Einstellung.</p>'; echo '</td></tr>'; // Category Base entfernen echo '<tr><th>Kategorie-Slug</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[remove_category_base]" value="1" ' . checked($o['remove_category_base'] ?? 0, 1, false) . ' /> '; echo 'Kategorie-Präfix aus URLs entfernen (<code>/category/</code> → <code>/</code>)</label>'; echo '<p class="description">Entfernt den <code>/category/</code> Slug aus Kategorie-URLs. Nach dem Speichern werden die Permalinks automatisch neu generiert.</p>'; echo '</td></tr>'; // Minify echo '<tr><th>JSON</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[minify]" value="1" ' . checked($o['minify'], true, false) . ' /> '; echo 'JSON minifizieren</label>'; echo '</td></tr>'; // ACF echo '<tr><th>ACF</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[acf_include]" value="1" ' . checked($o['acf_include'], true, false) . ' /> '; echo 'ACF-Felder aufnehmen</label>'; if (!$acf) { echo '<p class="description" style="color:#8a6d3b">ACF nicht aktiv.</p>'; } echo '</td></tr>'; // API Key + Prompt – KI-Feature: temporär ausgeblendet /* echo '<tr><th>OpenAI API Key</th><td>'; echo '<input type="password" name="' . esc_attr(self::OPT_KEY) . '[openai_api_key]" value="' . esc_attr(isset($o['openai_api_key']) ? $o['openai_api_key'] : '') . '" class="regular-text" />'; echo '<p class="description">Wird nur serverseitig verwendet.</p>'; echo '</td></tr>'; echo '<tr><th>KI Analyse Prompt</th><td>'; echo '<textarea name="' . esc_attr(self::OPT_KEY) . '[openai_prompt]" rows="5" style="width:100%;font-family:monospace;">'; echo esc_textarea($o['openai_prompt']); echo '</textarea>'; echo '<p class="description">System-Prompt für die KI-Analyse.</p>'; echo '</td></tr>'; */ echo '</table>'; submit_button('Speichern'); echo '</form>'; // ── Score-Gewichtung / Preset ────────────────────────────────────────── $preset = isset( $o['score_preset'] ) ? $o['score_preset'] : 'standard'; $presets_data = array( 'ymyl' => array( 'label' => 'YMYL / Experten', 'icon' => '🏥', 'desc' => 'Medizin, Recht, Finanzen, Beratung. E-E-A-T steht im Vordergrund.', 'tags' => 'Hebamme · Arzt · Anwalt · Finanzberater', 'content' => 30, 'eeat' => 40, 'technical' => 20, 'linking' => 10, ), 'standard' => array( 'label' => 'Standard Blog', 'icon' => '📝', 'desc' => 'Typischer Content-Blog. Ausgewogene Gewichtung.', 'tags' => 'Blog · Magazin · News · Tutorial', 'content' => 35, 'eeat' => 30, 'technical' => 20, 'linking' => 15, ), 'ecommerce' => array( 'label' => 'E-Commerce / Lokal', 'icon' => '🛒', 'desc' => 'Shop und lokale Unternehmen. Technik besonders wichtig.', 'tags' => 'Shop · Restaurant · Dienstleister · Agentur', 'content' => 30, 'eeat' => 20, 'technical' => 35, 'linking' => 15, ), 'custom' => array( 'label' => 'Individuell', 'icon' => '⚙️', 'desc' => 'Gewichtung manuell einstellen.', 'tags' => '', 'content' => $o['score_weight_content'] ?? 35, 'eeat' => $o['score_weight_eeat'] ?? 30, 'technical' => $o['score_weight_technical'] ?? 20, 'linking' => $o['score_weight_linking'] ?? 15, ), ); echo '<hr>'; echo '<h3>Score-Gewichtung</h3>'; echo '<p class="description">Wähle das Preset, das zu deiner Website passt, und klicke danach auf <strong>Speichern</strong>.</p>'; // Separates Formular nur für Score-Einstellungen echo '<form method="post" action="options.php" id="aije-score-form">'; settings_fields( self::OPT_KEY ); // Alle anderen Optionen als Hidden-Felder mitschicken foreach ( $o['enabled_post_types'] as $pt ) { echo '<input type="hidden" name="' . esc_attr( self::OPT_KEY ) . '[enabled_post_types][]" value="' . esc_attr( $pt ) . '" />'; } $bool_fields = array( 'enable_schema', 'minify', 'acf_include', 'cron_enabled', 'ping_enabled', 'ping_retry' ); foreach ( $bool_fields as $bf ) { echo '<input type="hidden" name="' . esc_attr( self::OPT_KEY ) . '[' . esc_attr( $bf ) . ']" value="' . esc_attr( $o[ $bf ] ) . '" />'; } $str_fields = array( 'cron_frequency', 'ping_secret', 'ping_timeout', 'openai_api_key', 'openai_prompt', 'seo_plugin' ); foreach ( $str_fields as $sf ) { echo '<input type="hidden" name="' . esc_attr( self::OPT_KEY ) . '[' . esc_attr( $sf ) . ']" value="' . esc_attr( $o[ $sf ] ?? '' ) . '" />'; } // ping_urls separat wegen Zeilenumbrüchen echo '<input type="hidden" name="' . esc_attr( self::OPT_KEY ) . '[ping_urls]" value="' . esc_attr( $o['ping_urls'] ?? '' ) . '" />'; // Radio-Inputs (versteckt – werden per JS gesteuert) foreach ( $presets_data as $key => $pd ) { echo '<input type="radio" id="aije-preset-radio-' . esc_attr( $key ) . '" name="' . esc_attr( self::OPT_KEY ) . '[score_preset]" value="' . esc_attr( $key ) . '" ' . checked( $preset, $key, false ) . ' style="display:none;" />'; } // Custom-Gewichte als Hidden (werden bei Custom per JS aktiviert) echo '<input type="hidden" id="aije-w-content" name="' . esc_attr( self::OPT_KEY ) . '[score_weight_content]" value="' . esc_attr( $o['score_weight_content'] ?? 35 ) . '" />'; echo '<input type="hidden" id="aije-w-eeat" name="' . esc_attr( self::OPT_KEY ) . '[score_weight_eeat]" value="' . esc_attr( $o['score_weight_eeat'] ?? 30 ) . '" />'; echo '<input type="hidden" id="aije-w-technical" name="' . esc_attr( self::OPT_KEY ) . '[score_weight_technical]" value="' . esc_attr( $o['score_weight_technical'] ?? 20 ) . '" />'; echo '<input type="hidden" id="aije-w-linking" name="' . esc_attr( self::OPT_KEY ) . '[score_weight_linking]" value="' . esc_attr( $o['score_weight_linking'] ?? 15 ) . '" />'; // Preset-Karten echo '<div style="display:flex;gap:12px;flex-wrap:wrap;margin:16px 0 24px;">'; foreach ( $presets_data as $key => $pd ) { $active = ( $preset === $key ); $border = $active ? '2px solid #0073aa' : '2px solid #ddd'; $bg = $active ? '#f0f8ff' : '#fff'; $check = $active ? '<span style="float:right;color:#0073aa;font-size:16px;">✔</span>' : ''; echo '<div id="aije-card-' . esc_attr( $key ) . '" style="flex:1;min-width:170px;max-width:230px;border:' . esc_attr( $border ) . ';border-radius:8px;padding:14px;background:' . esc_attr( $bg ) . ';cursor:pointer;box-sizing:border-box;">'; echo $check; echo '<div style="font-size:22px;margin-bottom:4px;">' . esc_html( $pd['icon'] ) . '</div>'; echo '<div style="font-weight:700;font-size:13px;margin-bottom:4px;">' . esc_html( $pd['label'] ) . '</div>'; echo '<div style="font-size:11px;color:#555;margin-bottom:8px;line-height:1.4;">' . esc_html( $pd['desc'] ) . '</div>'; if ( $key !== 'custom' ) { echo '<div style="font-size:10px;color:#888;line-height:1.8;">'; echo 'Content <strong>' . esc_html( $pd['content'] ) . '%</strong><br>'; echo 'E-E-A-T <strong>' . esc_html( $pd['eeat'] ) . '%</strong><br>'; echo 'Technik <strong>' . esc_html( $pd['technical'] ) . '%</strong><br>'; echo 'Vernetzung <strong>' . esc_html( $pd['linking'] ) . '%</strong>'; echo '</div>'; } if ( ! empty( $pd['tags'] ) ) { echo '<div style="margin-top:6px;font-size:9px;color:#999;">' . esc_html( $pd['tags'] ) . '</div>'; } echo '</div>'; // .aije-card } echo '</div>'; // Custom-Sliders echo '<div id="aije-custom-box" style="display:' . ( $preset === 'custom' ? 'block' : 'none' ) . ';background:#f8f8f8;border:1px solid #ddd;border-radius:6px;padding:16px;margin-bottom:16px;">'; echo '<p style="margin-top:0;font-size:12px;color:#555;">Werte werden auf <strong>100%</strong> normalisiert. Minimum je Kategorie: 5%.</p>'; $slider_fields = array( 'content' => array( 'label' => '✍ Content-Qualität', 'id' => 'aije-w-content', 'val' => $o['score_weight_content'] ?? 35 ), 'eeat' => array( 'label' => '🏅 E-E-A-T / Autorität', 'id' => 'aije-w-eeat', 'val' => $o['score_weight_eeat'] ?? 30 ), 'technical' => array( 'label' => '⚙ Technische SEO', 'id' => 'aije-w-technical', 'val' => $o['score_weight_technical'] ?? 20 ), 'linking' => array( 'label' => '🔗 Interne Vernetzung', 'id' => 'aije-w-linking', 'val' => $o['score_weight_linking'] ?? 15 ), ); foreach ( $slider_fields as $sf ) { echo '<div style="margin-bottom:12px;">'; echo '<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;">'; echo '<span>' . esc_html( $sf['label'] ) . '</span>'; echo '<strong id="' . esc_attr( $sf['id'] ) . '-display">' . esc_html( $sf['val'] ) . '%</strong>'; echo '</div>'; echo '<input type="range" min="5" max="70" value="' . esc_attr( $sf['val'] ) . '" style="width:100%;" data-hidden="' . esc_attr( $sf['id'] ) . '" data-display="' . esc_attr( $sf['id'] ) . '-display" />'; echo '</div>'; } echo '</div>'; // Speichern-Button echo '<p>'; submit_button( 'Einstellungen speichern', 'primary', 'submit', false ); echo '</p>'; echo '</form>'; // JS – inline nach den Elementen, kein DOMContentLoaded nötig $cards_json = wp_json_encode( array_keys( $presets_data ) ); echo '<script> (function(){ var keys = ' . $cards_json . '; function selectPreset(key) { // Radio setzen var radio = document.getElementById("aije-preset-radio-" + key); if (radio) radio.checked = true; // Alle Karten zurücksetzen keys.forEach(function(k) { var card = document.getElementById("aije-card-" + k); if (!card) return; card.style.border = "2px solid #ddd"; card.style.background = "#fff"; // Häkchen entfernen var chk = card.querySelector(".aije-check"); if (chk) chk.remove(); }); // Aktive Karte markieren var active = document.getElementById("aije-card-" + key); if (active) { active.style.border = "2px solid #0073aa"; active.style.background = "#f0f8ff"; var chk = document.createElement("span"); chk.className = "aije-check"; chk.style.cssText = "float:right;color:#0073aa;font-size:16px;"; chk.textContent = "✔"; active.insertBefore(chk, active.firstChild); } // Custom-Box ein/ausblenden var box = document.getElementById("aije-custom-box"); if (box) box.style.display = (key === "custom") ? "block" : "none"; } // Klick-Handler auf jede Karte keys.forEach(function(k) { var card = document.getElementById("aije-card-" + k); if (card) { card.addEventListener("click", function() { selectPreset(k); }); } }); // Slider → Hidden-Input + Anzeige document.querySelectorAll("input[type=range][data-hidden]").forEach(function(slider) { slider.addEventListener("input", function() { var hidden = document.getElementById(this.dataset.hidden); var display = document.getElementById(this.dataset.display); if (hidden) hidden.value = this.value; if (display) display.textContent = this.value + "%"; }); }); })(); </script>'; // ---- Hinweis auf Experten-Profil ---- echo '<hr>'; echo '<h3>Experten-Profile pflegen</h3>'; echo '<p>Die Berufsbezeichnung, Fachbiografie und SameAs-Links werden direkt im <a href="' . esc_url(admin_url('users.php')) . '">WordPress-Benutzerprofil</a> hinterlegt (Bereich: <em>Experten-Profil</em>). Das Plugin liest diese Daten automatisch aus und gibt sie als <code>application/ld+json</code> im <code><head></code> aus.</p>'; } private function tab_export() { $nonce = wp_create_nonce('aije_export'); $types = get_post_types(array('public' => true), 'objects'); $authors = get_users(array( 'who' => 'authors', 'orderby' => 'display_name', 'order' => 'ASC', 'fields' => array('ID', 'display_name'), )); $cats = get_terms(array( 'taxonomy' => 'category', 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', )); echo '<h2>' . esc_html__('Bulk Export', 'ai-json-extractor-auto-embed-lite') . '</h2>'; echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">'; echo '<input type="hidden" name="action" value="aije_export" />'; echo '<input type="hidden" name="_wpnonce" value="' . esc_attr($nonce) . '" />'; echo '<table class="form-table"><tbody>'; echo '<tr><th>' . esc_html__('Post-Typen', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; foreach ($types as $t) { echo '<label style="display:inline-block;margin-right:12px;">'; echo '<input type="checkbox" name="post_types[]" value="' . esc_attr($t->name) . '" checked /> '; echo esc_html($t->labels->singular_name . ' (' . $t->name . ')'); echo '</label>'; } echo '</td></tr>'; echo '<tr><th>' . esc_html__('Autor', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<select name="author"><option value="0">' . esc_html__('— Alle Autoren —', 'ai-json-extractor-auto-embed-lite') . '</option>'; foreach ($authors as $u) { echo '<option value="' . esc_attr($u->ID) . '">' . esc_html($u->display_name . ' (ID ' . $u->ID . ')') . '</option>'; } echo '</select></td></tr>'; echo '<tr><th>' . esc_html__('Kategorie', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<select name="cat"><option value="0">' . esc_html__('— Alle Kategorien —', 'ai-json-extractor-auto-embed-lite') . '</option>'; if (!is_wp_error($cats)) { foreach ($cats as $c) { echo '<option value="' . esc_attr($c->term_id) . '">' . esc_html($c->name . ' (ID ' . $c->term_id . ')') . '</option>'; } } echo '</select><p class="description">' . esc_html__('Hinweis: Kategorie-Filter wirkt nur auf den Post-Typ post.', 'ai-json-extractor-auto-embed-lite') . '</p></td></tr>'; echo '<tr><th>' . esc_html__('Zeitraum', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo esc_html__('Von', 'ai-json-extractor-auto-embed-lite') . ' <input type="date" name="date_from" /> '; echo esc_html__('bis', 'ai-json-extractor-auto-embed-lite') . ' <input type="date" name="date_to" />'; echo '</td></tr>'; echo '<tr><th>' . esc_html__('Suche', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<input type="text" name="s" class="regular-text" placeholder="' . esc_attr__('Suchbegriff (optional)', 'ai-json-extractor-auto-embed-lite') . '" />'; echo '</td></tr>'; echo '<tr><th>' . esc_html__('Optionen', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<label><input type="checkbox" name="include_acf" value="1" checked /> ' . esc_html__('ACF-Felder', 'ai-json-extractor-auto-embed-lite') . '</label><br />'; echo '<label><input type="checkbox" name="minify" value="1" /> ' . esc_html__('JSON minifizieren', 'ai-json-extractor-auto-embed-lite') . '</label><br />'; echo '<label><input type="checkbox" name="zip" value="1" checked /> ' . esc_html__('ZIP (1 JSON pro Beitrag)', 'ai-json-extractor-auto-embed-lite') . '</label>'; echo '</td></tr>'; echo '</tbody></table>'; echo '<p><button class="button button-primary">' . esc_html__('Jetzt exportieren', 'ai-json-extractor-auto-embed-lite') . '</button></p>'; echo '</form>'; if (isset($_GET['export_done'])) { $uploads = wp_upload_dir(); $file = isset($_GET['file']) ? sanitize_file_name(wp_unslash($_GET['file'])) : ''; $url = $file ? trailingslashit($uploads['baseurl']) . self::EXPORT_DIR . '/' . $file : ''; echo '<div class="notice notice-success"><p>'; echo esc_html__('Export abgeschlossen.', 'ai-json-extractor-auto-embed-lite') . ' '; echo esc_html__('Beiträge:', 'ai-json-extractor-auto-embed-lite') . ' ' . (int) $_GET['count'] . '. '; echo esc_html__('Größe:', 'ai-json-extractor-auto-embed-lite') . ' ' . esc_html(isset($_GET['size_kb']) ? wp_kses_post(wp_unslash($_GET['size_kb'])) : '–') . ' KB. '; if ($url) { echo esc_html__('Datei:', 'ai-json-extractor-auto-embed-lite') . ' <a href="' . esc_url($url) . '" target="_blank">' . esc_html($file) . '</a>'; } echo '</p></div>'; } } private function render_dashboard_list( $type, $filter = '' ) { $types = $this->get_opts()['enabled_post_types']; $excluded_ids = get_option( 'aije_excluded_posts', array() ); $posts = get_posts( array( 'post_type' => $types, 'posts_per_page' => -1, 'post_status' => 'publish', 'post__not_in' => $excluded_ids ?: array(0), ) ); $colors = array( 'bad' => '#e74c3c', 'medium' => '#e67e22', 'good' => '#27ae60', 'filter' => '#0073aa' ); // Filter-Mapping: filter-key → Issue-msg enthält diesen String $filter_map = array( 'no_image' => 'Beitragsbild', 'no_meta' => 'Meta-Description', 'too_short' => 'kurz', 'no_eeat' => 'Berufsbezeichnung', 'no_h2' => 'H2', 'no_internal' => 'Keine internen Links', ); echo '<ul style="margin-top:10px;">'; $found = false; foreach ( $posts as $post ) { $analysis = $this->calculate_ai_analysis( $post ); $score = $analysis['score']; // Score-Tabs $show = false; if ( $type === 'bad' && $score < 50 ) { $show = true; } if ( $type === 'medium' && $score >= 50 && $score < 75 ) { $show = true; } if ( $type === 'good' && $score >= 75 ) { $show = true; } if ( $type === 'filter' ) { $show = true; } if ( ! $show ) { continue; } // Filter anwenden if ( ! empty( $filter ) && isset( $filter_map[ $filter ] ) ) { $needle = $filter_map[ $filter ]; $matched = false; foreach ( $analysis['issues'] as $iss ) { if ( mb_stripos( $iss['msg'], $needle ) !== false ) { $matched = true; break; } } if ( ! $matched ) { continue; } } $found = true; // Top-3 Issues direkt in der Liste $top_issues = array_slice( $analysis['issues'], 0, 3 ); $sev_bg = array( 'high' => '#fdecea', 'medium' => '#fef9e7', 'low' => '#eafaf1' ); $cat_icons = array( 'content' => '✍', 'eeat' => '🏅', 'technical' => '⚙', 'linking' => '🔗' ); echo '<li id="aije-post-' . esc_attr($post->ID) . '" style="padding:10px 0;border-bottom:1px solid #f0f0f0;">'; echo '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;">'; echo '<div style="flex:1;min-width:0;">'; echo '<a href="' . esc_url( get_edit_post_link( $post->ID ) ) . '" target="_blank" style="font-weight:600;font-size:13px;">' . esc_html( $post->post_title ) . '</a>'; if ( ! empty( $top_issues ) && empty( $filter ) ) { echo '<div style="margin-top:5px;display:flex;flex-direction:column;gap:3px;">'; foreach ( $top_issues as $issue ) { $ic = $cat_icons[ $issue['category'] ] ?? '•'; $bg = $sev_bg[ $issue['severity'] ] ?? '#f5f5f5'; echo '<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 7px;background:' . esc_attr($bg) . ';border-radius:4px;width:fit-content;">'; echo '<span>' . $ic . '</span><span style="color:#333;">' . esc_html( $issue['msg'] ) . '</span>'; echo '</div>'; } echo '</div>'; } echo '</div>'; echo '<span style="font-size:20px;font-weight:700;color:' . esc_attr( $colors[ $type ] ) . ';white-space:nowrap;flex-shrink:0;">' . esc_html( $score ) . '</span>'; echo '<button type="button" onclick="aije_exclude_post(' . (int)$post->ID . ', this)" title="Ausschließen"' . ' style="flex-shrink:0;background:none;border:none;cursor:pointer;color:#ccc;font-size:16px;padding:0 2px;">🗑</button>'; echo '</div></li>'; } if ( ! $found ) { echo '<li style="color:#888;padding:8px 0;">Keine Artikel in dieser Kategorie.</li>'; } echo '</ul>'; } public function aije_exclude_post_handler() { check_ajax_referer( 'aije_dashboard_nonce', 'nonce' ); if ( ! current_user_can('manage_options') ) { wp_send_json_error(); } $post_id = intval( $_POST['post_id'] ?? 0 ); if ( ! $post_id ) { wp_send_json_error(); } $excluded = get_option( 'aije_excluded_posts', array() ); $excluded[] = $post_id; $excluded = array_unique( $excluded ); update_option( 'aije_excluded_posts', $excluded ); wp_send_json_success(); } public function aije_include_post_handler() { check_ajax_referer( 'aije_dashboard_nonce', 'nonce' ); if ( ! current_user_can('manage_options') ) { wp_send_json_error(); } $post_id = intval( $_POST['post_id'] ?? 0 ); $excluded = get_option( 'aije_excluded_posts', array() ); $excluded = array_values( array_filter( $excluded, fn($id) => $id !== $post_id ) ); update_option( 'aije_excluded_posts', $excluded ); wp_send_json_success(); } private function tab_dashboard() { wp_enqueue_script( 'chartjs', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true ); $types = $this->get_opts()['enabled_post_types']; $excluded_ids = get_option( 'aije_excluded_posts', array() ); $nonce_dash = wp_create_nonce( 'aije_dashboard_nonce' ); $query = new WP_Query( array( 'post_type' => $types, 'posts_per_page' => -1, 'post_status' => 'publish', 'post__not_in' => $excluded_ids, ) ); $scores = array(); $distribution = array( 'ready' => 0, 'mid' => 0, 'low' => 0 ); $cat_totals = array( 'content' => 0, 'eeat' => 0, 'technical' => 0, 'linking' => 0 ); $post_count = 0; if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); $p = get_post(); $analysis = $this->calculate_ai_analysis( $p ); $score = $analysis['score']; $scores[] = array( 'id' => get_the_ID(), 'title' => get_the_title(), 'score' => $score ); if ( $score >= 75 ) { $distribution['ready']++; } elseif ( $score >= 50 ) { $distribution['mid']++; } else { $distribution['low']++; } foreach ( $cat_totals as $k => $_ ) { $cat_totals[ $k ] += $analysis['categories'][ $k ]['score'] ?? 0; } $post_count++; } wp_reset_postdata(); } $avg = $post_count ? round( array_sum( array_column( $scores, 'score' ) ) / $post_count ) : 0; $cat_avgs = array(); foreach ( $cat_totals as $k => $total ) { $cat_avgs[ $k ] = $post_count ? round( $total / $post_count ) : 0; } usort( $scores, function( $a, $b ) { return $a['score'] <=> $b['score']; } ); $avg_color = $avg >= 75 ? '#27ae60' : ( $avg >= 50 ? '#e67e22' : '#e74c3c' ); echo '<h2 style="display:flex;align-items:center;gap:12px;">AI Readiness Dashboard ' . '<span style="font-size:14px;font-weight:400;background:' . esc_attr( $avg_color ) . ';color:#fff;padding:3px 10px;border-radius:20px;">' . esc_html( $post_count ) . ' Artikel · Ø ' . esc_html( $avg ) . ' Punkte</span></h2>'; echo '<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">'; echo '<div style="flex:0 0 200px;"><div style="font-size:11px;font-weight:600;color:#555;text-transform:uppercase;margin-bottom:8px;">Score-Verteilung</div>'; echo '<canvas id="aijeChart" style="max-width:200px;"></canvas></div>'; echo '<div style="flex:0 0 220px;"><div style="font-size:11px;font-weight:600;color:#555;text-transform:uppercase;margin-bottom:8px;">Kategorien-Ø</div>'; echo '<canvas id="aijeRadar" style="max-width:220px;"></canvas></div>'; echo '</div>'; echo '<script> document.addEventListener("DOMContentLoaded", function(){ new Chart(document.getElementById("aijeChart"), { type: "doughnut", data: { labels: ["KI-Ready (\u226575)", "Optimierbar (50–74)", "Kritisch (<50)"], datasets: [{ data: [' . (int)$distribution['ready'] . ',' . (int)$distribution['mid'] . ',' . (int)$distribution['low'] . '], backgroundColor: ["#27ae60","#e67e22","#e74c3c"], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: "bottom", labels: { font: { size: 11 } } } } } }); new Chart(document.getElementById("aijeRadar"), { type: "radar", data: { labels: ["Content", "E-E-A-T", "Technik", "Vernetzung"], datasets: [{ label: "\u00d8 Score", data: [' . (int)$cat_avgs['content'] . ',' . (int)$cat_avgs['eeat'] . ',' . (int)$cat_avgs['technical'] . ',' . (int)$cat_avgs['linking'] . '], backgroundColor: "rgba(52,152,219,.15)", borderColor: "#3498db", pointBackgroundColor: "#3498db", borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: true, scales: { r: { min: 0, max: 100, ticks: { stepSize: 25, font: { size: 10 } } } }, plugins: { legend: { display: false } } } }); const tabs = document.querySelectorAll(".aije-tab"); const contents = document.querySelectorAll(".aije-tab-content"); tabs.forEach(function(tab) { tab.addEventListener("click", function(){ tabs.forEach(function(t){ t.classList.remove("active"); }); contents.forEach(function(c){ c.classList.remove("active"); }); this.classList.add("active"); var el = document.getElementById("aije-tab-" + this.dataset.tab); if (el) el.classList.add("active"); }); }); }); </script>'; // Filter-Counts berechnen $filter_counts = array( 'no_image' => 0, 'no_meta' => 0, 'too_short' => 0, 'no_eeat' => 0, 'no_h2' => 0, 'no_internal' => 0, ); $filter_needles = array( 'no_image' => 'Beitragsbild', 'no_meta' => 'Meta-Description', 'too_short' => 'kurz', 'no_eeat' => 'Berufsbezeichnung', 'no_h2' => 'H2', 'no_internal' => 'Keine internen Links', ); $filter_labels = array( 'no_image' => '🖼 Kein Bild', 'no_meta' => '📝 Keine Meta', 'too_short' => '📏 Text zu kurz', 'no_eeat' => '🏅 Kein E-E-A-T', 'no_h2' => '📋 Kein H2', 'no_internal' => '🔗 Kein int. Linking', ); $all_posts_for_filter = get_posts( array( 'post_type' => $this->get_opts()['enabled_post_types'], 'posts_per_page' => -1, 'post_status' => 'publish', ) ); foreach ( $all_posts_for_filter as $fp ) { $fa = $this->calculate_ai_analysis( $fp ); foreach ( $filter_counts as $fk => $_ ) { foreach ( $fa['issues'] as $iss ) { if ( mb_stripos( $iss['msg'], $filter_needles[$fk] ) !== false ) { $filter_counts[$fk]++; break; } } } } echo '<h2 style="display:flex;align-items:center;justify-content:space-between;">Artikel-Qualität'; if ( ! empty($excluded_ids) ) { echo '<a href="' . esc_url( admin_url('tools.php?page=aije-lite&tab=dashboard&reset_excluded=1') ) . '"' . ' style="font-size:12px;font-weight:400;color:#888;text-decoration:none;border:1px solid #ddd;padding:4px 10px;border-radius:5px;">' . '↩ ' . count($excluded_ids) . ' ausgeschlossene Artikel zurücksetzen</a>'; } echo '</h2>'; // Reset-Handler if ( isset($_GET['reset_excluded']) ) { delete_option('aije_excluded_posts'); wp_safe_redirect( admin_url('tools.php?page=aije-lite&tab=dashboard') ); exit; } echo '<script>var aije_dash_nonce="' . esc_js($nonce_dash) . '";</script>'; // Score-Tabs echo '<div class="aije-tabs" style="margin-bottom:0;border-bottom:none;">'; echo '<button class="aije-tab active" data-tab="bad">Kritisch (' . (int)$distribution['low'] . ')</button>'; echo '<button class="aije-tab" data-tab="medium">Optimierbar (' . (int)$distribution['mid'] . ')</button>'; echo '<button class="aije-tab" data-tab="good">KI-Ready (' . (int)$distribution['ready'] . ')</button>'; echo '</div>'; // Filter-Buttons $excluded_count = count( get_option('aije_excluded_posts', array()) ); echo '<div class="aije-tabs" style="margin-top:6px;padding-top:6px;border-top:1px solid #eee;">'; foreach ( $filter_labels as $fk => $fl ) { if ( $filter_counts[$fk] > 0 ) { echo '<button class="aije-tab aije-filter-tab" data-tab="filter_' . esc_attr($fk) . '">'; echo esc_html($fl) . ' <span style="background:#e74c3c;color:#fff;font-size:10px;padding:1px 5px;border-radius:10px;margin-left:3px;">' . (int)$filter_counts[$fk] . '</span>'; echo '</button>'; } } if ( $excluded_count > 0 ) { echo '<button class="aije-tab aije-filter-tab" data-tab="excluded">'; echo '🚫 Ausgeschlossen <span style="background:#6b7280;color:#fff;font-size:10px;padding:1px 5px;border-radius:10px;margin-left:3px;">' . $excluded_count . '</span>'; echo '</button>'; } echo '</div>'; // Score-Tab Inhalte echo '<div id="aije-tab-bad" class="aije-tab-content active">'; $this->render_dashboard_list( 'bad' ); echo '</div>'; echo '<div id="aije-tab-medium" class="aije-tab-content">'; $this->render_dashboard_list( 'medium' ); echo '</div>'; echo '<div id="aije-tab-good" class="aije-tab-content">'; $this->render_dashboard_list( 'good' ); echo '</div>'; // Filter-Tab Inhalte foreach ( $filter_labels as $fk => $fl ) { echo '<div id="aije-tab-filter_' . esc_attr($fk) . '" class="aije-tab-content">'; echo '<p style="font-size:12px;color:#888;margin:8px 0 4px;">' . esc_html($fl) . ' – ' . (int)$filter_counts[$fk] . ' Artikel betroffen</p>'; $this->render_dashboard_list( 'filter', $fk ); echo '</div>'; } // Tab: Ausgeschlossene Artikel $excluded_ids = get_option( 'aije_excluded_posts', array() ); echo '<div id="aije-tab-excluded" class="aije-tab-content">'; if ( empty($excluded_ids) ) { echo '<p style="color:#888;padding:10px 0;">Keine ausgeschlossenen Artikel.</p>'; } else { echo '<ul style="margin-top:10px;">'; foreach ( $excluded_ids as $ex_id ) { $ex_post = get_post( $ex_id ); if ( ! $ex_post ) { continue; } echo '<li id="aije-post-' . esc_attr($ex_id) . '" style="padding:10px 0;border-bottom:1px solid #f0f0f0;display:flex;justify-content:space-between;align-items:center;gap:8px;">'; echo '<a href="' . esc_url( get_edit_post_link($ex_id) ) . '" target="_blank" style="font-weight:600;font-size:13px;">' . esc_html( $ex_post->post_title ) . '</a>'; echo '<button type="button" onclick="aije_include_post(' . (int)$ex_id . ', this)" title="Wieder einschließen"' . ' style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:5px;cursor:pointer;color:#15803d;font-size:12px;padding:4px 10px;white-space:nowrap;">↩ Einschließen</button>'; echo '</li>'; } echo '</ul>'; } echo '</div>'; $this->render_exclude_js(); } private function calculate_ai_score( $post ) { $analysis = $this->calculate_ai_analysis( $post ); return isset( $analysis['score'] ) ? (int) $analysis['score'] : 0; } private function render_exclude_js() { ?> <script> function aije_include_post(postId, btn) { var fd = new FormData(); fd.append('action', 'aije_include_post'); fd.append('nonce', aije_dash_nonce); fd.append('post_id', postId); fetch(ajaxurl, {method:'POST', body:fd, credentials:'same-origin'}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.success) { var li = document.getElementById('aije-post-' + postId); if (li) { li.style.opacity='0'; li.style.transition='opacity .3s'; setTimeout(function(){ li.remove(); },300); } } }); } function aije_exclude_post(postId, btn) { if (!confirm('Diesen Artikel aus der Analyse ausschließen?')) return; var fd = new FormData(); fd.append('action', 'aije_exclude_post'); fd.append('nonce', aije_dash_nonce); fd.append('post_id', postId); fetch(ajaxurl, {method:'POST', body:fd, credentials:'same-origin'}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.success) { var li = document.getElementById('aije-post-' + postId); if (li) { li.style.opacity = '0'; li.style.transition = 'opacity .3s'; setTimeout(function(){ li.remove(); }, 300); } } }); } </script> <?php } private function tab_automation() { $o = $this->get_opts(); $t = wp_next_scheduled(self::CRON_EXPORT); $nonce = wp_create_nonce('aije_export'); echo '<h2>' . esc_html__('Automation', 'ai-json-extractor-auto-embed-lite') . '</h2>'; echo '<form method="post" action="options.php">'; settings_fields(self::OPT_KEY); echo '<table class="form-table"><tbody>'; echo '<tr><th>' . esc_html__('Auto-Export', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[cron_enabled]" value="1" ' . checked($o['cron_enabled'], true, false) . ' /> ' . esc_html__('aktivieren', 'ai-json-extractor-auto-embed-lite') . '</label>'; echo '</td></tr>'; echo '<tr><th>' . esc_html__('Frequenz', 'ai-json-extractor-auto-embed-lite') . '</th><td>'; echo '<select name="' . esc_attr(self::OPT_KEY) . '[cron_frequency]">'; echo '<option value="daily" ' . selected($o['cron_frequency'], 'daily', false) . '>' . esc_html__('täglich', 'ai-json-extractor-auto-embed-lite') . '</option>'; echo '<option value="weekly" ' . selected($o['cron_frequency'], 'weekly', false) . '>' . esc_html__('wöchentlich', 'ai-json-extractor-auto-embed-lite') . '</option>'; echo '</select>'; echo '</td></tr>'; echo '</tbody></table>'; submit_button(esc_html__('Speichern', 'ai-json-extractor-auto-embed-lite')); echo '</form>'; echo '<p>' . esc_html__('Nächster Lauf:', 'ai-json-extractor-auto-embed-lite') . ' <strong>' . ($t ? esc_html(date_i18n('Y-m-d H:i:s', $t)) : '–') . '</strong></p>'; echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin-right:8px;">'; echo '<input type="hidden" name="action" value="aije_run_cron_now" />'; echo '<input type="hidden" name="_wpnonce" value="' . esc_attr($nonce) . '" />'; echo '<button class="button">' . esc_html__('Jetzt ausführen', 'ai-json-extractor-auto-embed-lite') . '</button>'; echo '</form>'; echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;">'; echo '<input type="hidden" name="action" value="aije_clear_cron" />'; echo '<input type="hidden" name="_wpnonce" value="' . esc_attr($nonce) . '" />'; echo '<button class="button">' . esc_html__('Planung zurücksetzen', 'ai-json-extractor-auto-embed-lite') . '</button>'; echo '</form>'; } private function tab_logs() { $uploads = wp_upload_dir(); $dir = trailingslashit($uploads['basedir']) . self::EXPORT_DIR; $log = trailingslashit($dir) . self::LOG_FILE; echo '<h2>' . esc_html__('Log Viewer', 'ai-json-extractor-auto-embed-lite') . '</h2>'; if (file_exists($log)) { echo '<table class="widefat striped" style="max-width:1200px;"><thead><tr>'; echo '<th>timestamp</th><th>filename</th><th>count</th><th>size_kb</th><th>filters</th>'; echo '</tr></thead><tbody>'; $lines = @file($log); if ($lines) { $lines = array_reverse($lines); $i = 0; foreach ($lines as $line) { $i++; if ($i > 200) { break; } $cols = str_getcsv(trim($line)); if (count($cols) >= 5) { echo '<tr>'; foreach (array_slice($cols, 0, 5) as $col) { echo '<td>' . esc_html($col) . '</td>'; } echo '</tr>'; } } } echo '</tbody></table>'; } else { echo '<p>' . esc_html__('Noch keine Logs vorhanden.', 'ai-json-extractor-auto-embed-lite') . '</p>'; } } private function tab_pinghub() { $o = $this->get_opts(); echo '<h2>' . esc_html__('PingHub', 'ai-json-extractor-auto-embed-lite') . '</h2>'; echo '<p>' . esc_html__('Benachrichtigt Webhooks/LLM-Endpoints bei Veröffentlichung/Aktualisierung.', 'ai-json-extractor-auto-embed-lite') . '</p>'; echo '<form method="post" action="options.php" style="max-width:900px;">'; settings_fields(self::OPT_KEY); echo '<table class="form-table"><tbody>'; echo '<tr><th>Aktiv</th><td>'; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[ping_enabled]" value="1" ' . checked($o['ping_enabled'], true, false) . ' /> aktivieren</label>'; echo '</td></tr>'; echo '<tr><th>Events</th><td>'; echo '<label style="margin-right:12px;"><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[ping_events][]" value="publish" ' . checked(in_array('publish', $o['ping_events'], true), true, false) . ' /> Veröffentlichung</label> '; echo '<label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[ping_events][]" value="update" ' . checked(in_array('update', $o['ping_events'], true), true, false) . ' /> Aktualisierung</label>'; echo '</td></tr>'; echo '<tr><th>Ping-URLs</th><td>'; echo '<textarea name="' . esc_attr(self::OPT_KEY) . '[ping_urls]" rows="12" style="width:100%;font-family:monospace;">' . esc_textarea($o['ping_urls']) . '</textarea>'; echo '<p class="description">Kommentarzeilen mit # werden ignoriert.</p>'; echo '</td></tr>'; echo '<tr><th>HMAC-Secret</th><td>'; echo '<input type="text" name="' . esc_attr(self::OPT_KEY) . '[ping_secret]" value="' . esc_attr($o['ping_secret']) . '" class="regular-text" placeholder="optional" />'; echo '<p class="description"><code>X-AIJE-Signature: sha256=<hash></code></p>'; echo '</td></tr>'; echo '<tr><th>Timeout & Retry</th><td>'; echo 'Timeout (Sek.): <input type="number" name="' . esc_attr(self::OPT_KEY) . '[ping_timeout]" value="' . esc_attr($o['ping_timeout']) . '" min="2" max="30" style="width:80px;" /> '; echo '  <label><input type="checkbox" name="' . esc_attr(self::OPT_KEY) . '[ping_retry]" value="1" ' . checked($o['ping_retry'], true, false) . ' /> fehlgeschlagene Pings in Queue</label>'; echo '</td></tr>'; echo '</tbody></table>'; submit_button('Speichern'); echo '</form>'; $errs = get_option(self::PING_ERRORS_OPT, array()); if (is_array($errs) && !empty($errs)) { echo '<h3>Letzte Ping-Fehler</h3>'; echo '<table class="widefat striped" style="max-width:1000px;"><thead><tr><th>Zeit</th><th>URL</th><th>Status/Fehler</th><th>Event</th><th>Post-ID</th></tr></thead><tbody>'; $i = 0; foreach (array_reverse($errs) as $e) { $i++; if ($i > 50) { break; } echo '<tr>'; echo '<td>' . esc_html($e['time']) . '</td>'; echo '<td>' . esc_html($e['url']) . '</td>'; echo '<td>' . esc_html($e['error']) . '</td>'; echo '<td>' . esc_html($e['event']) . '</td>'; echo '<td>' . esc_html($e['post_id']) . '</td>'; echo '</tr>'; } echo '</tbody></table>'; } } private function tab_status() { $o = $this->get_opts(); $uploads = wp_upload_dir(); $ok = isset($uploads['basedir']) && is_dir($uploads['basedir']) && is_writable($uploads['basedir']); $licensed = self::license_is_valid(); echo '<h2>' . esc_html__('Status & Hilfe', 'ai-json-extractor-auto-embed-lite') . '</h2>'; echo '<table class="widefat striped" style="max-width:800px;">'; echo '<tr><td><strong>Plugin</strong></td><td>WP Rankify (AI Auto Embed) v1.2.6</td></tr>'; echo '<tr><td><strong>WordPress</strong></td><td>' . esc_html(get_bloginfo('version')) . '</td></tr>'; echo '<tr><td><strong>PHP</strong></td><td>' . esc_html(PHP_VERSION) . '</td></tr>'; echo '<tr><td><strong>Upload-Ordner</strong></td><td>' . ($ok ? 'beschreibbar' : 'NICHT beschreibbar') . '</td></tr>'; echo '<tr><td><strong>PingHub aktiv</strong></td><td>' . (!empty($o['ping_enabled']) ? 'Ja' : 'Nein') . '</td></tr>'; echo '<tr><td><strong>Lizenz</strong></td><td>' . ($licensed ? 'aktiv' : 'nicht aktiv') . '</td></tr>'; echo '</table>'; } /** * Hook für künftige Pro-Features. Aktuell nicht im Einsatz. */ private function render_pro_locked( $feature ) { // Reserviert für zukünftige Premium-Features } public function handle_gsc_oauth_callback() { if ( ! isset($_GET['page']) || $_GET['page'] !== 'aije-lite' ) { return; } if ( ! isset($_GET['tab']) || $_GET['tab'] !== 'gsc' ) { return; } // OAuth Callback if ( isset($_GET['gsc_callback']) && isset($_GET['code']) ) { $o = $this->get_opts(); $client_id = $o['gsc_client_id'] ?? ''; $client_secret = $o['gsc_client_secret'] ?? ''; $redirect_uri = admin_url( 'tools.php?page=aije-lite&tab=gsc&gsc_callback=1' ); $resp = wp_remote_post( 'https://oauth2.googleapis.com/token', array( 'body' => array( 'code' => sanitize_text_field( $_GET['code'] ), 'client_id' => $client_id, 'client_secret' => $client_secret, 'redirect_uri' => $redirect_uri, 'grant_type' => 'authorization_code', ), )); if ( ! is_wp_error($resp) ) { $token = json_decode( wp_remote_retrieve_body($resp), true ); if ( ! empty($token['access_token']) ) { update_option( 'aije_gsc_access_token', $token['access_token'] ); update_option( 'aije_gsc_refresh_token', $token['refresh_token'] ?? '' ); update_option( 'aije_gsc_token_expiry', time() + ( $token['expires_in'] ?? 3600 ) ); wp_safe_redirect( admin_url('tools.php?page=aije-lite&tab=gsc&gsc_connected=1') ); exit; } } wp_safe_redirect( admin_url('tools.php?page=aije-lite&tab=gsc&gsc_error=1') ); exit; } // Trennen if ( isset($_GET['gsc_disconnect']) ) { delete_option('aije_gsc_access_token'); delete_option('aije_gsc_refresh_token'); delete_option('aije_gsc_token_expiry'); wp_safe_redirect( admin_url('tools.php?page=aije-lite&tab=gsc') ); exit; } } private function tab_gsc() { $o = $this->get_opts(); $client_id = $o['gsc_client_id'] ?? ''; $client_secret= $o['gsc_client_secret'] ?? ''; $access_token = $this->get_valid_gsc_token(); $redirect_uri = admin_url( 'tools.php?page=aije-lite&tab=gsc&gsc_callback=1' ); // Erfolgsmeldungen if ( isset($_GET['gsc_connected']) ) { echo '<div class="notice notice-success is-dismissible"><p>✓ Google Search Console erfolgreich verbunden!</p></div>'; } if ( isset($_GET['gsc_error']) ) { echo '<div class="notice notice-error"><p>Fehler beim Verbinden. Bitte prüfe Client-ID und Secret.</p></div>'; } // Verbinden-Button if ( isset($_POST['aije_gsc_save_nonce']) && wp_verify_nonce($_POST['aije_gsc_save_nonce'], 'aije_gsc_save') ) { $o['gsc_client_id'] = sanitize_text_field( $_POST['gsc_client_id'] ?? '' ); $o['gsc_client_secret'] = sanitize_text_field( $_POST['gsc_client_secret'] ?? '' ); update_option( self::OPT_KEY, $o ); $client_id = $o['gsc_client_id']; $client_secret = $o['gsc_client_secret']; echo '<div class="notice notice-success"><p>✓ Zugangsdaten gespeichert.</p></div>'; } echo '<div style="max-width:780px;margin-top:16px;">'; // ── VERBUNDEN: Daten anzeigen ───────────────────────────────────────── if ( $access_token ) { echo '<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:16px 20px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;">'; echo '<span style="color:#15803d;font-weight:700;">✓ Google Search Console verbunden</span>'; echo '<a href="' . esc_url(admin_url('tools.php?page=aije-lite&tab=gsc&gsc_disconnect=1')) . '" style="font-size:12px;color:#888;text-decoration:none;">Verbindung trennen</a>'; echo '</div>'; $this->render_gsc_data( $access_token ); echo '</div>'; return; } // ── NICHT VERBUNDEN: Schritt-für-Schritt Anleitung ─────────────────── echo '<div style="background:#fff8f0;border:1px solid #f29400;border-radius:10px;padding:20px 24px;margin-bottom:24px;">'; echo '<div style="font-size:16px;font-weight:700;margin-bottom:4px;">🔍 Google Search Console verbinden</div>'; echo '<p style="font-size:13px;color:#555;margin:0;">Sieh direkt im Plugin welche Keywords deine Artikel ranken, wie oft sie geklickt werden und wo sie in den Suchergebnissen stehen.</p>'; echo '</div>'; // Schritt-Container $steps = array( array( 'num' => '1', 'title' => 'Google Cloud Projekt erstellen', 'desc' => 'Öffne die Google Cloud Console und erstelle ein neues Projekt (oder wähle ein bestehendes).', 'link' => 'https://console.cloud.google.com/projectcreate', 'btn' => 'Google Cloud Console öffnen →', 'hint' => 'Projektname: z.B. „WP Rankify". Klicke dann auf <strong>Erstellen</strong>.', ), array( 'num' => '2', 'title' => 'Search Console API aktivieren', 'desc' => 'Aktiviere die Google Search Console API für dein Projekt.', 'link' => 'https://console.cloud.google.com/apis/library/searchconsole.googleapis.com', 'btn' => 'Search Console API aktivieren →', 'hint' => 'Klicke auf den blauen <strong>Aktivieren</strong> Button.', ), array( 'num' => '3', 'title' => 'OAuth-Zustimmungsbildschirm konfigurieren', 'desc' => 'Richte den OAuth-Zustimmungsbildschirm ein (nur einmalig nötig).', 'link' => 'https://console.cloud.google.com/apis/credentials/consent', 'btn' => 'OAuth-Zustimmung einrichten →', 'hint' => 'Wähle <strong>Extern</strong> → App-Name eintragen → Support-E-Mail eintragen → Speichern. Unter <strong>Bereiche</strong> nichts ändern.', ), array( 'num' => '3b', 'title' => 'Testnutzer eintragen', 'desc' => 'Solange die App nicht bei Google verifiziert ist, musst du dich selbst als Testnutzer eintragen.', 'link' => 'https://console.cloud.google.com/auth/audience', 'btn' => 'Testnutzer verwalten →', 'hint' => 'Klicke auf <strong>„+ Add Users"</strong> und trage die Google-E-Mail-Adresse ein mit der du dich verbinden möchtest. Ohne diesen Schritt erscheint beim Verbinden „Zugriff blockiert".', ), array( 'num' => '4', 'title' => 'OAuth-Zugangsdaten erstellen', 'desc' => 'Erstelle OAuth 2.0 Client-IDs für dein Plugin.', 'link' => 'https://console.cloud.google.com/apis/credentials/oauthclient', 'btn' => 'Zugangsdaten erstellen →', 'hint' => 'Anwendungstyp: <strong>Webanwendung</strong>. Name: „WP Rankify". Unter <strong>Autorisierte Weiterleitungs-URIs</strong> folgende URL eintragen:', 'copy' => $redirect_uri, ), ); foreach ( $steps as $step ) { echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:12px;display:flex;gap:16px;">'; echo '<div style="flex-shrink:0;width:32px;height:32px;background:#f29400;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;">' . esc_html($step['num']) . '</div>'; echo '<div style="flex:1;">'; echo '<div style="font-size:14px;font-weight:700;margin-bottom:4px;">' . esc_html($step['title']) . '</div>'; echo '<p style="font-size:13px;color:#555;margin:0 0 10px 0;">' . esc_html($step['desc']) . '</p>'; if ( ! empty($step['hint']) ) { echo '<div style="background:#f8f9fa;border-radius:6px;padding:10px 14px;font-size:12px;color:#555;margin-bottom:10px;">💡 ' . wp_kses_post($step['hint']) . '</div>'; } if ( ! empty($step['copy']) ) { echo '<div style="background:#1e1e1e;color:#d4d4d4;border-radius:6px;padding:10px 14px;font-size:12px;font-family:monospace;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">'; echo '<span>' . esc_html($step['copy']) . '</span>'; echo '<button type="button" onclick="navigator.clipboard.writeText(\'' . esc_js($step['copy']) . '\');this.textContent=\'✓ Kopiert\';" style="background:#f29400;color:#fff;border:none;border-radius:4px;padding:4px 10px;font-size:11px;cursor:pointer;">Kopieren</button>'; echo '</div>'; } echo '<a href="' . esc_url($step['link']) . '" target="_blank" style="display:inline-block;background:#111;color:#fff;padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;text-decoration:none;">' . esc_html($step['btn']) . '</a>'; echo '</div></div>'; } // Schritt 5: Client-ID + Secret eintragen echo '<div style="background:#fff;border:2px solid #f29400;border-radius:10px;padding:20px 24px;margin-bottom:24px;">'; echo '<div style="display:flex;gap:16px;">'; echo '<div style="flex-shrink:0;width:32px;height:32px;background:#f29400;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;">5</div>'; echo '<div style="flex:1;">'; echo '<div style="font-size:14px;font-weight:700;margin-bottom:4px;">Client-ID und Client-Secret eintragen</div>'; echo '<p style="font-size:13px;color:#555;margin:0 0 14px 0;">Nach dem Erstellen der Zugangsdaten zeigt Google dir Client-ID und Client-Secret an. Trage sie hier ein.</p>'; echo '<form method="post">'; wp_nonce_field('aije_gsc_save','aije_gsc_save_nonce'); echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">'; echo '<div><label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">Client-ID</label>'; echo '<input type="text" name="gsc_client_id" value="' . esc_attr($client_id) . '" placeholder="XXXXXXXXXXXX.apps.googleusercontent.com" style="width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:12px;font-family:monospace;"></div>'; echo '<div><label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;">Client-Secret</label>'; echo '<input type="text" autocomplete="new-password" name="gsc_client_secret" value="' . esc_attr($client_secret) . '" placeholder="GOCSPX-..." style="width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:12px;font-family:monospace;"></div>'; echo '</div>'; echo '<button type="submit" style="background:#111;color:#fff;border:none;padding:9px 20px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Speichern & weiter</button>'; echo '</form></div></div></div>'; // Schritt 6: Mit Google verbinden if ( $client_id && $client_secret ) { $oauth_url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query(array( 'client_id' => $client_id, 'redirect_uri' => $redirect_uri, 'response_type'=> 'code', 'scope' => 'https://www.googleapis.com/auth/webmasters.readonly', 'access_type' => 'offline', 'prompt' => 'consent', )); echo '<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:20px 24px;display:flex;gap:16px;">'; echo '<div style="flex-shrink:0;width:32px;height:32px;background:#22c55e;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;">6</div>'; echo '<div style="flex:1;">'; echo '<div style="font-size:14px;font-weight:700;margin-bottom:4px;color:#15803d;">Mit Google verbinden</div>'; echo '<p style="font-size:13px;color:#555;margin:0 0 14px 0;">Jetzt mit deinem Google-Account verbinden der Zugriff auf die Search Console hat.</p>'; echo '<a href="' . esc_url($oauth_url) . '" style="display:inline-flex;align-items:center;gap:8px;background:#4285f4;color:#fff;padding:10px 20px;border-radius:6px;font-size:13px;font-weight:600;text-decoration:none;">🔑 Mit Google verbinden</a>'; echo '</div></div>'; } echo '</div>'; } private function refresh_gsc_token() { $o = $this->get_opts(); $client_id = $o['gsc_client_id'] ?? ''; $client_secret = $o['gsc_client_secret'] ?? ''; $refresh_token = get_option( 'aije_gsc_refresh_token', '' ); if ( ! $refresh_token || ! $client_id || ! $client_secret ) { return false; } $resp = wp_remote_post( 'https://oauth2.googleapis.com/token', array( 'body' => array( 'client_id' => $client_id, 'client_secret' => $client_secret, 'refresh_token' => $refresh_token, 'grant_type' => 'refresh_token', ), 'timeout' => 10, )); if ( is_wp_error($resp) ) { return false; } $token = json_decode( wp_remote_retrieve_body($resp), true ); if ( empty($token['access_token']) ) { return false; } update_option( 'aije_gsc_access_token', $token['access_token'] ); update_option( 'aije_gsc_token_expiry', time() + ( $token['expires_in'] ?? 3600 ) ); return $token['access_token']; } private function get_valid_gsc_token() { $access_token = get_option( 'aije_gsc_access_token', '' ); $expiry = (int) get_option( 'aije_gsc_token_expiry', 0 ); // Token läuft in weniger als 5 Minuten ab oder ist bereits abgelaufen if ( $expiry && time() > $expiry - 300 ) { $new_token = $this->refresh_gsc_token(); if ( $new_token ) { return $new_token; } // Refresh fehlgeschlagen → Token löschen → neu verbinden delete_option( 'aije_gsc_access_token' ); return ''; } return $access_token; } private function render_gsc_data( $access_token ) { // Token ggf. erneuern $access_token = $this->get_valid_gsc_token(); if ( ! $access_token ) { echo '<div class="notice notice-warning"><p>⚠ Google Token abgelaufen. Bitte <a href="' . esc_url(admin_url('tools.php?page=aije-lite&tab=gsc&gsc_disconnect=1')) . '">Verbindung trennen</a> und neu verbinden.</p></div>'; return; } // Property-Liste holen $resp = wp_remote_get( 'https://www.googleapis.com/webmasters/v3/sites', array( 'headers' => array( 'Authorization' => 'Bearer ' . $access_token ), 'timeout' => 10, )); if ( is_wp_error($resp) ) { echo '<p style="color:#ef4444;">Fehler beim Abrufen der Properties.</p>'; return; } $sites = json_decode( wp_remote_retrieve_body($resp), true ); // Prüfen ob Token abgelaufen (401) $http_code = wp_remote_retrieve_response_code($resp); if ( $http_code === 401 ) { $new_token = $this->refresh_gsc_token(); if ( $new_token ) { // Nochmal versuchen mit neuem Token $resp = wp_remote_get( 'https://www.googleapis.com/webmasters/v3/sites', array( 'headers' => array( 'Authorization' => 'Bearer ' . $new_token ), 'timeout' => 10, )); $sites = json_decode( wp_remote_retrieve_body($resp), true ); } else { delete_option('aije_gsc_access_token'); echo '<div class="notice notice-warning"><p>⚠ Google Token abgelaufen. Bitte <a href="' . esc_url(admin_url('tools.php?page=aije-lite&tab=gsc&gsc_disconnect=1')) . '">Verbindung trennen</a> und neu verbinden.</p></div>'; return; } } $entries = $sites['siteEntry'] ?? array(); if ( empty($entries) ) { echo '<p style="color:#888;">Keine Search Console Properties gefunden. Stelle sicher dass deine Website in der Google Search Console verifiziert ist.</p>'; return; } // Property auswählen $selected = get_option('aije_gsc_property', $entries[0]['siteUrl'] ?? ''); if ( isset($_POST['gsc_property']) ) { $selected = sanitize_text_field( $_POST['gsc_property'] ); update_option('aije_gsc_property', $selected); } // Property-Auswahl echo '<div style="margin-bottom:20px;display:flex;align-items:center;gap:10px;">'; echo '<label style="font-size:13px;font-weight:600;">Property:</label>'; $form_action = add_query_arg( array_filter(array( 'page' => 'aije-lite', 'tab' => 'gsc', 'gsc_days' => $_GET['gsc_days'] ?? null, 'gsc_sort' => $_GET['gsc_sort'] ?? null, 'gsc_dir' => $_GET['gsc_dir'] ?? null, )), admin_url('tools.php') ); echo '<form method="post" action="' . esc_url($form_action) . '" style="display:inline;">'; echo '<select name="gsc_property" onchange="this.form.submit()" style="padding:7px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;">'; foreach ( $entries as $site ) { $url = $site['siteUrl'] ?? ''; echo '<option value="' . esc_attr($url) . '" ' . selected($selected,$url,false) . '>' . esc_html($url) . '</option>'; } echo '</select></form></div>'; // Zeitraum aus GET-Parameter $days_map = array('7'=>7, '28'=>28, '90'=>90); $days = isset($_GET['gsc_days']) && isset($days_map[$_GET['gsc_days']]) ? (int)$_GET['gsc_days'] : 28; // GSC liefert Daten mit ~2 Tagen Verzögerung → gestern als Endpunkt $end = date('Y-m-d', strtotime('-2 days')); $start = date('Y-m-d', strtotime('-' . ($days + 2) . ' days')); // Vergleichszeitraum (gleiche Länge davor) $cmp_end = date('Y-m-d', strtotime('-' . ($days + 2) . ' days')); $cmp_start = date('Y-m-d', strtotime('-' . ($days * 2 + 2) . ' days')); $body = wp_json_encode(array( 'startDate' => $start, 'endDate' => $end, 'dimensions' => array('page'), 'rowLimit' => 500, 'orderBy' => array(array('fieldName'=>'clicks','sortOrder'=>'DESCENDING')), )); $resp2 = wp_remote_post( 'https://www.googleapis.com/webmasters/v3/sites/' . urlencode($selected) . '/searchAnalytics/query', array( 'headers' => array('Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json'), 'body' => $body, 'timeout' => 15, ) ); $data2 = json_decode( wp_remote_retrieve_body($resp2), true ); $rows = $data2['rows'] ?? array(); // Vergleichszeitraum $cmp_body = wp_json_encode(array( 'startDate' => $cmp_start, 'endDate' => $cmp_end, 'dimensions' => array('page'), 'rowLimit' => 500, )); $resp3 = wp_remote_post( 'https://www.googleapis.com/webmasters/v3/sites/' . urlencode($selected) . '/searchAnalytics/query', array( 'headers' => array('Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json'), 'body' => $cmp_body, 'timeout' => 15, ) ); $cmp_rows = json_decode( wp_remote_retrieve_body($resp3), true )['rows'] ?? array(); // Vergleichsdaten indexieren: url → data $cmp_index = array(); foreach ( $cmp_rows as $cr ) { $cmp_index[$cr['keys'][0]] = $cr; } // Zeitraum-Umschalter echo '<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">'; echo '<span style="font-size:12px;color:#888;">Zeitraum:</span>'; foreach ( array('7'=>'7 Tage','28'=>'28 Tage','90'=>'90 Tage') as $d => $label ) { $active = ($days == $d); $url_d = add_query_arg( array('gsc_days'=>$d, 'gsc_sort'=>$_GET['gsc_sort']??'clicks', 'gsc_dir'=>$_GET['gsc_dir']??'desc'), admin_url('tools.php?page=aije-lite&tab=gsc') ); echo '<a href="' . esc_url($url_d) . '" style="padding:4px 12px;border-radius:5px;font-size:12px;font-weight:600;text-decoration:none;' . ($active ? 'background:#f29400;color:#fff;' : 'background:#f3f4f6;color:#555;') . '">' . esc_html($label) . '</a>'; } echo '<span style="font-size:11px;color:#aaa;margin-left:8px;">' . count($rows) . ' Seiten · ' . esc_html($start) . ' – ' . esc_html($end) . '</span>'; echo '</div>'; if ( empty($rows) ) { echo '<p style="color:#888;">Keine Daten gefunden.</p>'; return; } // Issues direkt aus Post-Daten berechnen (ohne gecachten Score) $o = $this->get_opts(); $types = $o['enabled_post_types'] ?? array('post'); $all_posts = get_posts( array('post_type'=>$types,'posts_per_page'=>-1,'post_status'=>'publish') ); $url_issues = array(); foreach ( $all_posts as $p ) { $issues = array(); // Kein Beitragsbild if ( ! has_post_thumbnail($p->ID) ) { $issues[] = array('msg'=>'Kein Beitragsbild','severity'=>'medium'); } // Keine Meta-Description $meta = get_post_meta($p->ID,'_aije_metadesc',true) ?: get_post_meta($p->ID,'_yoast_wpseo_metadesc',true) ?: get_post_meta($p->ID,'rank_math_description',true); if ( empty($meta) ) { $issues[] = array('msg'=>'Keine Meta-Description','severity'=>'high'); } // Text zu kurz $content = wp_strip_all_tags($p->post_content); if ( str_word_count($content) < 300 ) { $issues[] = array('msg'=>'Text zu kurz','severity'=>'medium'); } // Kein H2 if ( ! preg_match('/<h2/i', $p->post_content) ) { $issues[] = array('msg'=>'Kein H2','severity'=>'medium'); } // Keine internen Links if ( strpos( $p->post_content, home_url() ) === false ) { $issues[] = array('msg'=>'Kein internes Linking','severity'=>'medium'); } // Kein E-E-A-T $job = get_user_meta( $p->post_author, 'aije_job_title', true ); if ( empty($job) ) { $issues[] = array('msg'=>'Kein E-E-A-T Profil','severity'=>'high'); } if ( ! empty($issues) ) { $purl = trailingslashit( get_permalink($p->ID) ); $url_issues[$purl] = $issues; } } // ── Gesamtstatistiken ────────────────────────────────────────────────── $total_clicks = array_sum( array_column( $rows, 'clicks' ) ); $total_impr = array_sum( array_column( $rows, 'impressions' ) ); // Gewichteter Durchschnitt (wie GSC): nach Impressionen gewichtet $total_impr_for_avg = array_sum( array_column( $rows, 'impressions' ) ); if ( $total_impr_for_avg > 0 ) { $weighted_ctr = 0; $weighted_pos = 0; foreach ( $rows as $r ) { $impr_weight = ($r['impressions'] ?? 0) / $total_impr_for_avg; $weighted_ctr += ($r['ctr'] ?? 0) * $impr_weight; $weighted_pos += ($r['position'] ?? 0) * $impr_weight; } $avg_ctr = round( $weighted_ctr * 100, 1 ); $avg_pos = round( $weighted_pos, 1 ); } else { $avg_ctr = 0; $avg_pos = 0; } $cmp_total_clicks = array_sum( array_column( $cmp_rows, 'clicks' ) ); $cmp_total_impr = array_sum( array_column( $cmp_rows, 'impressions' ) ); $cmp_impr_total = array_sum( array_column( $cmp_rows, 'impressions' ) ); if ( $cmp_impr_total > 0 ) { $cmp_w_pos = 0; foreach ( $cmp_rows as $r ) { $cmp_w_pos += ($r['position'] ?? 0) * ( ($r['impressions'] ?? 0) / $cmp_impr_total ); } $cmp_avg_pos = round( $cmp_w_pos, 1 ); } else { $cmp_avg_pos = 0; } $positions_won = 0; $positions_lost = 0; foreach ( $rows as $row ) { $url = $row['keys'][0] ?? ''; $cmp = $cmp_index[$url] ?? null; if ( ! $cmp ) { continue; } $delta = round($row['position'] ?? 0, 1) - round($cmp['position'] ?? 0, 1); if ( $delta < -0.5 ) { $positions_won++; } elseif ( $delta > 0.5 ) { $positions_lost++; } } $dc_clicks = $total_clicks - $cmp_total_clicks; $dc_impr = $total_impr - $cmp_total_impr; $dc_pos = $avg_pos - $cmp_avg_pos; $fmt_delta = function($v, $invert=false) { if ($v == 0) return '<span style="color:#aaa;">±0</span>'; $pos = $invert ? $v < 0 : $v > 0; $c = $pos ? '#22c55e' : '#ef4444'; return '<span style="color:' . $c . ';font-weight:600;">' . ($v > 0 ? '+' : '') . round($v,1) . '</span>'; }; echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:20px;">'; $kpi = array( array('Klicks', number_format_i18n($total_clicks), $fmt_delta($dc_clicks), ''), array('Impressionen', number_format_i18n($total_impr), $fmt_delta($dc_impr), ''), array('Ø CTR', $avg_ctr . '%', '', ''), array('Ø Position', $avg_pos, $fmt_delta(round($dc_pos,1), true), ''), array('📈 Gewonnen', $positions_won . ' Seiten', '', '#22c55e'), array('📉 Verloren', $positions_lost . ' Seiten', '', '#ef4444'), ); foreach ( $kpi as $s ) { echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;">'; echo '<div style="font-size:11px;color:#888;margin-bottom:4px;">' . esc_html($s[0]) . '</div>'; echo '<div style="font-size:22px;font-weight:700;color:' . esc_attr($s[3] ?: '#111') . ';line-height:1.2;">' . esc_html($s[1]) . '</div>'; if ( $s[2] ) { echo '<div style="font-size:11px;margin-top:3px;">vs. Vorperiode: ' . $s[2] . '</div>'; } echo '</div>'; } echo '</div>'; // Suchfeld echo '<input type="text" id="aije-gsc-search" placeholder="Seite filtern…" style="width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;margin-bottom:10px;" oninput="aije_gsc_filter(this.value)">'; echo '<div style="overflow-x:auto;">'; echo '<table id="aije-gsc-table" style="width:100%;border-collapse:collapse;font-size:13px;">'; echo '<thead><tr style="border-bottom:2px solid #e5e7eb;">'; $cols = array( 'page' => array('Seite', 'left', ''), 'tasks' => array('Tasks', 'left', ''), 'clicks' => array('Klicks', 'right', 'desc'), 'clicks_d' => array('±', 'right', ''), 'impr' => array('Impressionen', 'right', ''), 'ctr' => array('CTR', 'right', ''), 'pos' => array('Ø Position', 'right', ''), 'pos_d' => array('±', 'right', ''), ); foreach ( $cols as $key => $col ) { $align = $col[1]; $sort = $col[2] ? "aije_gsc_sort('$key',this)" : ($key !== 'page' && $key !== 'tasks' ? "aije_gsc_sort('$key',this)" : ''); $no_sort = in_array($key, array('tasks')); $cursor = ! $no_sort ? 'cursor:pointer;user-select:none;' : ''; $onclick = ! $no_sort ? ' onclick="aije_gsc_sort(\'' . esc_js($key) . '\',this)"' : ''; echo '<th style="text-align:' . $align . ';padding:8px 4px;color:#888;font-weight:600;' . $cursor . '"' . $onclick . ' data-col="' . $key . '">'; echo esc_html($col[0]); if ( ! $no_sort ) { echo ' <span class="aije-sort-icon" style="font-size:10px;opacity:.5;">↕</span>'; } echo '</th>'; } echo '</tr></thead><tbody id="aije-gsc-tbody">'; foreach ( $rows as $row ) { $url = $row['keys'][0] ?? ''; $url_trail = trailingslashit($url); $path = str_replace( home_url(), '', $url ); $clicks = (int)($row['clicks'] ?? 0); $impr = (int)($row['impressions'] ?? 0); $ctr_raw = round( ($row['ctr'] ?? 0) * 100, 1 ); $pos = round( $row['position'] ?? 0, 1 ); $pos_c = $pos <= 3 ? '#22c55e' : ($pos <= 10 ? '#f59e0b' : '#ef4444'); // Tasks für diese URL $issues = $url_issues[$url_trail] ?? array(); $task_html = ''; $task_count = 0; if ( ! empty($issues) ) { foreach ( $issues as $issue ) { $sev = $issue['severity'] ?? 'medium'; $sev_c = $sev === 'high' ? '#ef4444' : ($sev === 'medium' ? '#f59e0b' : '#888'); $task_html .= '<span title="' . esc_attr($issue['msg']) . '" style="display:inline-block;background:' . $sev_c . '22;color:' . $sev_c . ';border:1px solid ' . $sev_c . '44;border-radius:4px;padding:1px 6px;font-size:10px;margin:1px;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;">' . esc_html( mb_substr($issue['msg'], 0, 25) . (mb_strlen($issue['msg']) > 25 ? '…' : '') ) . '</span>'; $task_count++; } } echo '<tr style="border-bottom:1px solid #f3f4f6;" data-url="' . esc_attr(strtolower($path)) . '">'; echo '<td style="padding:8px 4px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'; echo '<a href="' . esc_url($url) . '" target="_blank" style="color:#3b82f6;text-decoration:none;" title="' . esc_attr($path) . '">' . esc_html( strlen($path) > 40 ? substr($path,0,37).'…' : $path ) . '</a>'; echo '</td>'; echo '<td style="padding:8px 4px;max-width:200px;">'; if ( $task_html ) { echo '<div style="display:flex;flex-wrap:wrap;gap:2px;">' . $task_html . '</div>'; } else { echo '<span style="color:#22c55e;font-size:11px;">✓ OK</span>'; } echo '</td>'; // Vergleichsdaten $cmp = $cmp_index[$url] ?? null; $d_clicks = $cmp ? $clicks - (int)($cmp['clicks'] ?? 0) : null; $d_pos = $cmp ? $pos - round($cmp['position'] ?? 0, 1) : null; $delta_clicks = ''; if ( $d_clicks !== null ) { $dc = $d_clicks > 0 ? '+' . $d_clicks : (string)$d_clicks; $dc_c = $d_clicks > 0 ? '#22c55e' : ($d_clicks < 0 ? '#ef4444' : '#888'); $delta_clicks = '<span style="color:' . $dc_c . ';font-weight:600;">' . $dc . '</span>'; } $delta_pos = ''; if ( $d_pos !== null && $d_pos != 0 ) { $dp = round($d_pos, 1); $dp_str = $dp > 0 ? '+' . $dp : (string)$dp; $dp_c = $dp < 0 ? '#22c55e' : '#ef4444'; $delta_pos = '<span style="color:' . $dp_c . ';font-weight:600;">' . $dp_str . '</span>'; } echo '<td style="text-align:right;padding:8px 4px;font-weight:600;" data-val="' . $clicks . '">' . number_format_i18n($clicks) . '</td>'; echo '<td style="text-align:right;padding:8px 4px;font-size:11px;" data-val="' . ($d_clicks ?? 0) . '">' . ($delta_clicks ?: '<span style="color:#ccc;">–</span>') . '</td>'; echo '<td style="text-align:right;padding:8px 4px;color:#888;" data-val="' . $impr . '">' . number_format_i18n($impr) . '</td>'; echo '<td style="text-align:right;padding:8px 4px;" data-val="' . $ctr_raw . '">' . $ctr_raw . '%</td>'; echo '<td style="text-align:right;padding:8px 4px;font-weight:700;color:' . $pos_c . ';" data-val="' . $pos . '">' . $pos . '</td>'; echo '<td style="text-align:right;padding:8px 4px;font-size:11px;" data-val="' . ($d_pos ?? 0) . '">' . ($delta_pos ?: '<span style="color:#ccc;">–</span>') . '</td>'; echo '</tr>'; } echo '</tbody></table></div>'; ?> <script> var aije_gsc_sort_col = 'clicks'; var aije_gsc_sort_dir = -1; function aije_gsc_sort(col, th) { if (aije_gsc_sort_col === col) { aije_gsc_sort_dir *= -1; } else { aije_gsc_sort_col = col; aije_gsc_sort_dir = -1; } document.querySelectorAll('#aije-gsc-table .aije-sort-icon').forEach(function(el){ el.textContent = '↕'; el.style.opacity='.5'; }); if (th) { var icon = th.querySelector('.aije-sort-icon'); if(icon){ icon.textContent = aije_gsc_sort_dir < 0 ? '↓' : '↑'; icon.style.opacity='1'; } } var tbody = document.getElementById('aije-gsc-tbody'); var rows = Array.from(tbody.querySelectorAll('tr')); var colIdx = {'page':0,'tasks':1,'clicks':2,'clicks_d':3,'impr':4,'ctr':5,'pos':6,'pos_d':7}; var idx = colIdx[col] ?? 2; rows.sort(function(a,b){ var av = a.cells[idx] ? (a.cells[idx].dataset.val !== undefined ? parseFloat(a.cells[idx].dataset.val) : a.cells[idx].textContent.trim()) : 0; var bv = b.cells[idx] ? (b.cells[idx].dataset.val !== undefined ? parseFloat(b.cells[idx].dataset.val) : b.cells[idx].textContent.trim()) : 0; if (typeof av === 'string') return aije_gsc_sort_dir * av.localeCompare(bv); return aije_gsc_sort_dir * (av - bv); }); rows.forEach(function(r){ tbody.appendChild(r); }); } function aije_gsc_filter(val) { val = val.toLowerCase(); document.querySelectorAll('#aije-gsc-tbody tr').forEach(function(r){ r.style.display = r.dataset.url.indexOf(val) !== -1 ? '' : 'none'; }); } // Sortierung aus URL lesen (function(){ var params = new URLSearchParams(window.location.search); var col = params.get('gsc_sort') || 'clicks'; var dir = params.get('gsc_dir') || 'desc'; aije_gsc_sort_col = col; aije_gsc_sort_dir = dir === 'asc' ? 1 : -1; document.addEventListener('DOMContentLoaded', function(){ var th = document.querySelector('#aije-gsc-table th[data-col="' + col + '"]'); aije_gsc_sort(col, th); }); })(); // Bei Sortierung URL aktualisieren (ohne Reload) var _orig_sort = aije_gsc_sort; aije_gsc_sort = function(col, th) { _orig_sort(col, th); var url = new URL(window.location.href); url.searchParams.set('gsc_sort', col); url.searchParams.set('gsc_dir', aije_gsc_sort_dir < 0 ? 'desc' : 'asc'); history.replaceState(null, '', url.toString()); }; </script> <?php } private function tab_llms() { $nonce = wp_create_nonce( 'aije_llms_save' ); if ( isset($_POST['aije_llms_nonce']) && wp_verify_nonce($_POST['aije_llms_nonce'], 'aije_llms_save') ) { update_option( 'aije_llms_enabled', ! empty($_POST['llms_enabled']) ? 1 : 0 ); update_option( 'aije_llms_custom', sanitize_textarea_field( wp_unslash($_POST['llms_custom'] ?? '') ) ); update_option( 'aije_llms_block_bots', ! empty($_POST['llms_block_bots']) ? 1 : 0 ); // Rewrite Flush nach Aktivierung flush_rewrite_rules(); echo '<div class="notice notice-success is-dismissible"><p>✓ llms.txt Einstellungen gespeichert.</p></div>'; } $enabled = get_option( 'aije_llms_enabled', 1 ); $custom = get_option( 'aije_llms_custom', '' ); $block_bots = get_option( 'aije_llms_block_bots', 0 ); $llms_url = home_url( '/llms.txt' ); $preview = $this->generate_llms_content(); echo '<div style="max-width:760px;margin-top:16px;">'; // Status echo '<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">'; if ( $enabled ) { echo '<span style="background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;padding:5px 14px;border-radius:20px;font-size:12px;font-weight:700;">✓ llms.txt aktiv</span>'; echo '<a href="' . esc_url($llms_url) . '" target="_blank" style="font-size:12px;color:#3b82f6;">' . esc_html($llms_url) . ' ↗</a>'; } else { echo '<span style="background:#f9fafb;color:#888;border:1px solid #e5e7eb;padding:5px 14px;border-radius:20px;font-size:12px;">llms.txt deaktiviert</span>'; } echo '</div>'; echo '<form method="post">'; echo '<input type="hidden" name="aije_llms_nonce" value="' . esc_attr($nonce) . '">'; echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:16px;">'; echo '<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">'; echo '<input type="checkbox" name="llms_enabled" value="1" ' . checked($enabled,1,false) . ' style="width:16px;height:16px;">'; echo '<div><div style="font-size:14px;font-weight:700;">llms.txt aktivieren</div>'; echo '<div style="font-size:12px;color:#888;margin-top:2px;">Erstellt automatisch eine <code>/llms.txt</code> Datei die KI-Systemen erklärt wer du bist und was deine Website bietet.</div></div></label></div>'; echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:16px;">'; echo '<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">'; echo '<input type="checkbox" name="llms_block_bots" value="1" ' . checked($block_bots,1,false) . ' style="width:16px;height:16px;">'; echo '<div><div style="font-size:14px;font-weight:700;">Aggressive KI-Crawler blockieren</div>'; echo '<div style="font-size:12px;color:#888;margin-top:2px;">Trägt GPTBot, CCBot, anthropic-ai u.a. in die <code>robots.txt</code> ein.</div></div></label></div>'; echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:24px;">'; echo '<div style="font-size:14px;font-weight:700;margin-bottom:6px;">Zusätzliche Informationen (optional)</div>'; echo '<div style="font-size:12px;color:#888;margin-bottom:8px;">Eigener Text der am Ende der llms.txt eingefügt wird. Markdown ist erlaubt.</div>'; echo '<textarea name="llms_custom" rows="4" style="width:100%;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;font-family:monospace;">' . esc_textarea($custom) . '</textarea></div>'; echo '<button type="submit" style="background:#f29400;color:#fff;border:none;padding:11px 28px;border-radius:7px;font-size:14px;font-weight:600;cursor:pointer;">Einstellungen speichern</button>'; echo '</form>'; echo '<div style="margin-top:24px;">'; echo '<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#888;margin-bottom:8px;">Vorschau</div>'; echo '<pre style="background:#1e1e1e;color:#d4d4d4;padding:20px;border-radius:8px;font-size:12px;line-height:1.7;overflow-x:auto;white-space:pre-wrap;">' . esc_html($preview) . '</pre>'; echo '</div></div>'; } public function generate_llms_content() { $site_name = get_bloginfo( 'name' ); $site_desc = get_bloginfo( 'description' ); $site_url = home_url(); $o = $this->get_opts(); $custom = get_option( 'aije_llms_custom', '' ); $lines = array(); $lines[] = '# ' . $site_name; if ( $site_desc ) { $lines[] = '> ' . $site_desc; } $lines[] = ''; $lines[] = '## Website'; $lines[] = '- URL: ' . $site_url; $lines[] = '- Sprache: ' . get_bloginfo('language'); $lines[] = ''; $admins = get_users( array( 'role' => 'administrator', 'number' => 1 ) ); if ( $admins ) { $admin = $admins[0]; $job = get_user_meta( $admin->ID, self::USER_META_JOB_TITLE, true ); $bio = get_user_meta( $admin->ID, self::USER_META_EXPERTISE, true ); $same_as = get_user_meta( $admin->ID, self::USER_META_SAME_AS, true ); $lines[] = '## Autor'; $lines[] = '- Name: ' . $admin->display_name; if ( $job ) { $lines[] = '- Expertise: ' . $job; } if ( $bio ) { $lines[] = '- Biografie: ' . wp_trim_words($bio, 25); } if ( $same_as ) { foreach ( preg_split("/ | | /", $same_as) as $link ) { $link = trim($link); if ( $link && substr($link,0,1) !== '#' ) { $lines[] = '- Profil: ' . $link; } } } $lines[] = ''; } $posts = get_posts( array( 'post_type' => $o['enabled_post_types'], 'posts_per_page' => 10, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ) ); if ( $posts ) { $lines[] = '## Aktuelle Inhalte'; foreach ( $posts as $p ) { $lines[] = '- [' . get_the_title($p) . '](' . get_permalink($p) . ')'; } $lines[] = ''; } $lines[] = '## Nutzungshinweise'; $lines[] = '- Zitate mit Quellenangabe erlaubt'; $lines[] = '- Kommerzielle Nutzung ohne Genehmigung nicht gestattet'; $lines[] = '- Generiert mit WP Rankify (https://wprankify.de)'; if ( $custom ) { $lines[] = ''; $lines[] = $custom; } return implode( " ", $lines ); } public function remove_category_base_rewrite() { $o = $this->get_opts(); if ( empty($o['remove_category_base']) ) { // Wenn deaktiviert: category_link Filter entfernen return; } // Nur category_link Filter – KEINE Rewrite-Rules die REST API brechen // Die URL-Umschreibung passiert über .htaccess (wird beim Speichern generiert) add_filter( 'category_link', function( $link, $cat_id ) { return preg_replace( '#/category/#', '/', $link ); }, 10, 2 ); // request Filter: /slug/ → category_name=slug (für WordPress intern) add_filter( 'request', function( $query_vars ) { return $query_vars; }); } /** * Schreibt .htaccess Regel für Category-Base-Entfernung * Wird beim Speichern der Einstellungen aufgerufen */ public function update_htaccess_category_base( $enabled ) { $htaccess = ABSPATH . '.htaccess'; if ( ! is_writable($htaccess) ) { return; } $content = file_get_contents($htaccess); $marker_start = '# BEGIN AIJE Category Base'; $marker_end = '# END AIJE Category Base'; // Alten Block entfernen $content = preg_replace( '/' . preg_quote($marker_start, '/') . '.*?' . preg_quote($marker_end, '/') . '\s*/s', '', $content ); if ( $enabled ) { $rule = $marker_start . " " . "<IfModule mod_rewrite.c> " . "RewriteEngine On " . "RewriteCond %{REQUEST_URI} !^/wp-json/ " . "RewriteCond %{REQUEST_URI} !^/wp-admin/ " . "RewriteCond %{REQUEST_URI} !^/wp-content/ " . "RewriteCond %{REQUEST_URI} !^/wp-includes/ " . "RewriteRule ^(.+)/category/([^/]+)/?$ /$1/$2/ [R=301,L] " . "</IfModule> " . $marker_end . " "; // Vor dem WordPress Block einfügen $content = $rule . $content; } file_put_contents($htaccess, $content); } public function register_llms_rewrite() { add_rewrite_rule( '^llms\.txt$', 'index.php?aije_llms=1', 'top' ); add_rewrite_tag( '%aije_llms%', '([0-9]+)' ); } public function serve_llms_txt() { if ( ! get_query_var('aije_llms') ) { return; } if ( ! get_option('aije_llms_enabled', 1) ) { return; } nocache_headers(); header( 'Content-Type: text/plain; charset=utf-8' ); echo $this->generate_llms_content(); exit; } public function append_llms_to_robots( $output, $public ) { if ( get_option('aije_llms_block_bots', 0) ) { $bots = array('GPTBot','CCBot','anthropic-ai','Claude-Web','Bytespider','FacebookBot','Applebot-Extended'); foreach ( $bots as $bot ) { $output .= " User-agent: " . $bot . " Disallow: / "; } } if ( get_option('aije_llms_enabled', 1) ) { $output .= " # LLMs LLMs: " . home_url('/llms.txt') . " "; } return $output; } private function tab_license() { // Live-Status vom Proxy holen $pro_status = $this->get_pro_status(); $tier = $pro_status['tier'] ?? 'free'; $is_pro = ! empty($pro_status['is_pro']); $remaining = $pro_status['remaining'] ?? 0; $sub_id = $pro_status['subscription_id'] ?? ''; $since = $pro_status['since'] ?? ''; $tier_colors = array('free'=>'#888','starter'=>'#22c55e','pro'=>'#f29400','agency'=>'#3b82f6'); $tier_color = $tier_colors[$tier] ?? '#888'; $tier_names = array('free'=>'Free','starter'=>'Starter','pro'=>'Pro','agency'=>'Agency'); $tier_name = $tier_names[$tier] ?? ucfirst($tier); echo '<h2>Lizenz & Abo-Status</h2>'; echo '<div style="max-width:640px;">'; // Status-Card echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:20px;">'; echo '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">'; echo '<div>'; echo '<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#888;margin-bottom:4px;">Aktiver Plan</div>'; echo '<div style="display:flex;align-items:center;gap:10px;">'; echo '<span id="aije-license-tier-name" style="font-size:24px;font-weight:800;color:' . esc_attr($tier_color) . ';">' . esc_html($tier_name) . '</span>'; if ( $is_pro ) { echo '<span id="aije-license-badge" style="background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;">✓ Aktiv</span>'; } else { echo '<span id="aije-license-badge" style="background:#f9f9f9;color:#888;border:1px solid #e5e7eb;padding:3px 10px;border-radius:20px;font-size:11px;">Kostenlos</span>'; } echo '</div>'; echo '</div>'; // Refresh-Button echo '<button type="button" id="aije-license-refresh" style="background:#f3f4f6;border:1px solid #e5e7eb;border-radius:6px;padding:8px 14px;font-size:12px;cursor:pointer;color:#555;"> ↻ Status aktualisieren </button>'; echo '</div>'; // Details echo '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; $rows = array( array('Domain', home_url()), array('KI-Analysen', $tier === 'agency' ? 'Unlimitiert' : ( $tier === 'pro' ? (int)$remaining . ' verbleibend · 100/Monat' : ( $tier === 'starter' ? (int)$remaining . ' verbleibend · 50/Monat' : (int)$remaining . ' verbleibend · 3/Monat (Free)' ) ) ), ); if ( $sub_id ) { $rows[] = array('Abo-ID', $sub_id); } if ( $since ) { $rows[] = array('Aktiv seit', date_i18n('d.m.Y', strtotime($since))); } foreach ( $rows as $idx => $row ) { echo '<tr style="border-top:1px solid #f3f4f6;">'; echo '<td style="padding:8px 0;color:#888;width:140px;">' . esc_html($row[0]) . '</td>'; $cell_id = $row[0] === 'KI-Analysen' ? ' id="aije-license-ki"' : ''; echo '<td' . $cell_id . ' style="padding:8px 0;font-weight:500;color:#111;">' . esc_html($row[1]) . '</td>'; echo '</tr>'; } echo '</table>'; echo '</div>'; // Upgrade-Box wenn Free if ( ! $is_pro ) { echo '<div style="background:linear-gradient(135deg,#fff8f0,#fff3e0);border:1px solid #f29400;border-radius:10px;padding:20px 24px;margin-bottom:20px;">'; echo '<div style="font-size:15px;font-weight:700;margin-bottom:8px;">🚀 Auf Pro upgraden</div>'; echo '<p style="font-size:13px;color:#555;margin-bottom:16px;">Mehr KI-Analysen pro Monat. Jederzeit kündbar.</p>'; echo '<div style="display:flex;gap:10px;flex-wrap:wrap;">'; echo '<a href="https://wprankify.de/#pricing" target="_blank" style="background:#f29400;color:#fff;padding:10px 20px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;"> Starter – 2,99€/Monat →</a>'; echo '<a href="https://wprankify.de/#pricing" target="_blank" style="background:#111;color:#fff;padding:10px 20px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none;"> Pro – 4,99€/Monat →</a>'; echo '</div></div>'; } // Aktivierungs-Box echo '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;">'; echo '<div style="font-size:13px;font-weight:600;margin-bottom:10px;">Abo bereits gebucht?</div>'; echo '<p style="font-size:12px;color:#888;margin-bottom:10px;">Trage deine PayPal Subscription-ID ein. Du findest sie in der Bestätigungsmail von PayPal (Format: I-XXXXXXXXXXXX).</p>'; echo '<div style="display:flex;gap:8px;flex-wrap:wrap;">'; echo '<input type="text" id="aije-sub-id" placeholder="I-XXXXXXXXXXXX" style="flex:1;min-width:200px;padding:9px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;font-family:monospace;" />'; echo '<button type="button" id="aije-activate-btn" style="background:#111;color:#fff;border:none;padding:9px 18px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;"> Aktivieren →</button>'; echo '</div>'; echo '<div id="aije-activate-result" style="margin-top:10px;font-size:12px;"></div>'; echo '</div>'; echo '</div>'; // JS: Refresh-Button $nonce = wp_create_nonce('aije_topic_nonce'); ?> <script> document.addEventListener('DOMContentLoaded', function(){ // Aktivierungs-Button var activateBtn = document.getElementById('aije-activate-btn'); var activateResult = document.getElementById('aije-activate-result'); if (activateBtn) { activateBtn.addEventListener('click', function(){ var subId = document.getElementById('aije-sub-id').value.trim(); if (!subId || !subId.match(/^I-[A-Z0-9]+$/i)) { activateResult.innerHTML = '<span style="color:#ef4444;">Bitte gib eine gültige Subscription-ID ein (Format: I-XXXXXXXXXXXX)</span>'; return; } activateBtn.disabled = true; activateBtn.textContent = 'Wird aktiviert…'; activateResult.innerHTML = ''; fetch('https://www.muench-media.com/aije-proxy/activate.php', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'subscription_id=' + encodeURIComponent(subId) + '&domain=' + encodeURIComponent('<?= esc_js(home_url()) ?>') }) .then(function(r){ return r.text(); }) .then(function(html){ activateBtn.disabled = false; activateBtn.textContent = 'Aktivieren →'; if (html.indexOf('aktiviert') !== -1 || html.indexOf('success') !== -1 || html.indexOf('Pro') !== -1) { activateResult.innerHTML = '<span style="color:#15803d;font-weight:600;">✓ Aktivierung erfolgreich! Status wird aktualisiert…</span>'; // Cache löschen und Status neu laden setTimeout(function(){ var fd = new FormData(); fd.append('action','aije_check_status'); fd.append('nonce','<?= esc_js($nonce) ?>'); fetch(ajaxurl,{method:'POST',body:fd,credentials:'same-origin'}) .then(function(r){return r.json();}) .then(function(d){ if(d.success){ location.reload(); } }); }, 1500); } else { activateResult.innerHTML = '<span style="color:#ef4444;">Aktivierung fehlgeschlagen. Bitte prüfe die Subscription-ID.</span>'; } }) .catch(function(){ activateBtn.disabled = false; activateBtn.textContent = 'Aktivieren →'; activateResult.innerHTML = '<span style="color:#ef4444;">Verbindungsfehler. Bitte erneut versuchen.</span>'; }); }); } var btn = document.getElementById('aije-license-refresh'); if (!btn) return; btn.addEventListener('click', function(){ btn.textContent = '…'; btn.disabled = true; var fd = new FormData(); fd.append('action','aije_check_status'); fd.append('nonce','<?= esc_js($nonce) ?>'); fetch(ajaxurl,{method:'POST',body:fd,credentials:'same-origin'}) .then(function(r){return r.json();}) .then(function(d){ btn.textContent = '↻ Status aktualisieren'; btn.disabled = false; if (d.success && d.data) { var data = d.data; var tier = data.tier || 'free'; var used = data.used || 0; var remaining = data.remaining || 0; var limit = data.limit || 0; var colors = {free:'#888',starter:'#22c55e',pro:'#f29400',agency:'#3b82f6'}; var names = {free:'Free',starter:'Starter',pro:'Pro',agency:'Agency'}; var c = colors[tier] || '#888'; // Tier-Name aktualisieren var tierEl = document.querySelector('[id="aije-license-tier-name"]'); if (tierEl) { tierEl.textContent = names[tier] || tier; tierEl.style.color = c; } // KI-Analysen aktualisieren var kiEl = document.querySelector('[id="aije-license-ki"]'); if (kiEl) { kiEl.textContent = used + ' verwendet · ' + remaining + ' verbleibend · ' + limit + '/Monat'; } // Aktiv-Badge var badgeEl = document.querySelector('[id="aije-license-badge"]'); if (badgeEl) { badgeEl.textContent = tier !== 'free' ? '✓ Aktiv' : 'Kostenlos'; } // Fallback: Seite neu laden location.reload(); } else { btn.textContent = '✗ Fehler – nochmal versuchen'; } }) .catch(function(){ btn.textContent = '↻ Status aktualisieren'; btn.disabled = false; }); }); }); </script> <?php } public static function validate_license_remote( $license_key ) { if ( empty( $license_key ) ) { return array( 'valid' => false, 'error' => 'Kein Lizenzcode angegeben.' ); } $home = function_exists( 'home_url' ) ? home_url() : ''; $parsed = $home ? parse_url( $home ) : array(); $host = strtolower( $parsed['host'] ?? '' ); if ( strpos( $host, 'www.' ) === 0 ) { $host = substr( $host, 4 ); } $response = wp_remote_post( 'https://www.muench-media.com/aije-license/validate', array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'license_key' => $license_key, 'domain' => $host ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'valid' => false, 'error' => 'Server nicht erreichbar: ' . $response->get_error_message() ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return is_array( $body ) ? $body : array( 'valid' => false, 'error' => 'Ungültige Server-Antwort.' ); } /* ===== Metaboxes ===== */ public function add_toggle_metabox() { $types = $this->get_opts()['enabled_post_types']; foreach ($types as $t) { add_meta_box( 'aije_toggle', esc_html__('AI JSON – Ausgabe', 'ai-json-extractor-auto-embed-lite'), array($this, 'render_toggle_metabox'), $t, 'side', 'default' ); } } public function render_toggle_metabox($post) { $no = get_post_meta($post->ID, self::META_NO_EMBED, true); wp_nonce_field('aije_toggle', 'aije_toggle_nonce'); echo '<label><input type="checkbox" name="aije_no_embed" value="1" ' . checked($no, true, false) . ' /> '; echo esc_html__('Ausgabe für diesen Beitrag deaktivieren', 'ai-json-extractor-auto-embed-lite') . '</label>'; echo '<p class="description" style="margin-top:6px;">Deaktiviert sowohl JSON-Embed als auch Person-Schema für diesen Beitrag.</p>'; } public function save_post_meta($post_id) { if (!isset($_POST['aije_toggle_nonce']) || !wp_verify_nonce(wp_unslash($_POST['aije_toggle_nonce']), 'aije_toggle')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!current_user_can('edit_post', $post_id)) { return; } $no = !empty($_POST['aije_no_embed']) ? '1' : ''; if ($no) { update_post_meta($post_id, self::META_NO_EMBED, '1'); } else { delete_post_meta($post_id, self::META_NO_EMBED); } } public function add_preview_metabox() { $types = $this->get_opts()['enabled_post_types']; foreach ($types as $t) { add_meta_box( 'aije_preview', esc_html__('AI JSON – Vorschau (readonly)', 'ai-json-extractor-auto-embed-lite'), array('AIJE_Lite_100', 'render_preview_metabox_cb'), $t, 'side', 'default' ); } } public static function render_preview_metabox_cb($post) { $self = new self(); $data = $self->extract_payload($post); $flags = 0; if (defined('JSON_UNESCAPED_UNICODE')) { $flags |= JSON_UNESCAPED_UNICODE; } if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } if (defined('JSON_PRETTY_PRINT')) { $flags |= JSON_PRETTY_PRINT; } $json = function_exists('wp_json_encode') ? wp_json_encode($data, $flags) : json_encode($data); echo '<textarea readonly style="width:100%;height:260px;font-family:monospace;background:#f7f7f7;border:1px solid #ddd;">' . esc_textarea($json) . '</textarea>'; $pto = get_post_type_object($post->post_type); $base = (!empty($pto->rest_base)) ? $pto->rest_base : $post->post_type; $api_url = rest_url('wp/v2/' . $base . '/' . $post->ID); if (!empty($api_url)) { $is_published = ($post->post_status === 'publish'); $url_out = $is_published ? $api_url : add_query_arg('context', 'edit', $api_url); echo '<p style="margin-top:8px;">'; echo '<a href="' . esc_url($url_out) . '" target="_blank" class="button button-secondary">REST API öffnen</a>'; if (!$is_published) { echo '<br><small style="color:#666;">Hinweis: Beitrag ist nicht veröffentlicht. Login erforderlich.</small>'; } echo '</p>'; } } /* ===== Frontend Auto-Embed (JSON-Blob) ===== */ public function auto_embed() { if (!function_exists('is_singular') || !is_singular()) { return; } global $post; if (empty($post) || !is_object($post)) { return; } $o = $this->get_opts(); if (!in_array($post->post_type, $o['enabled_post_types'], true)) { return; } if (get_post_meta($post->ID, self::META_NO_EMBED, true)) { return; } $data = $this->extract_payload($post); $flags = 0; if (defined('JSON_UNESCAPED_UNICODE')) { $flags |= JSON_UNESCAPED_UNICODE; } if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } if (empty($o['minify']) && defined('JSON_PRETTY_PRINT')) { $flags |= JSON_PRETTY_PRINT; } $json = function_exists('wp_json_encode') ? wp_json_encode($data, $flags) : json_encode($data); echo "\n<!-- AI Auto Embed: auto-embed start -->\n"; echo '<script type="application/json" id="ai-json-extractor" data-post-id="' . esc_attr((string) $post->ID) . '">' . $json . '</script>' . "\n"; if (!empty($o['enable_schema'])) { echo '<script type="application/ld+json" id="ai-json-extractor-schema">' . $this->schema_article($post) . '</script>' . "\n"; } echo "<!-- AI AUTO EMBED: auto-embed end -->\n"; } private function extract_payload($post) { $o = $this->get_opts(); $u = get_userdata($post->post_author); $acf = array(); $content = apply_filters('the_content', $post->post_content); $text = wp_strip_all_tags($content); $lines = preg_split("/\r\n|\n|\r/", $text); $paragraphs = array(); foreach ($lines as $line) { $line = trim(preg_replace('/\s+/u', ' ', $line)); if ($line !== '') { $paragraphs[] = $line; } } $links = array(); if (preg_match_all('/<a[^>]+href=[\'\"]?([^\'\" >]+)[\'\"]?[^>]*>(.*?)<\/a>/i', $content, $m, PREG_SET_ORDER)) { foreach ($m as $a) { $href = trim($a[1]); $txt = trim(wp_strip_all_tags($a[2])); if ($href !== '') { $links[] = array('text' => $txt, 'href' => $href); } } } if (!empty($o['acf_include']) && function_exists('get_fields')) { $acf_fields = get_fields($post->ID); if (is_array($acf_fields) && !empty($acf_fields)) { $acf = $acf_fields; } } $data = array( 'title' => get_the_title($post), 'author' => $u ? $u->display_name : null, 'date' => get_post_time('Y-m-d', true, $post), 'paragraphs' => $paragraphs, 'links' => $links, 'meta' => array( 'post_id' => (int) $post->ID, 'site' => get_bloginfo('name'), 'url' => get_permalink($post), 'lang' => get_bloginfo('language'), ), ); // ---- NEU: Experten-Profil-Daten im JSON-Blob mitliefern ---- $expert = $this->get_author_expert_data($post->post_author); if (!empty($expert)) { $data['author_expert'] = $expert; } if (!empty($acf)) { $data['acf'] = $acf; } return $data; } private function schema_article($post) { $site = get_bloginfo('name'); $url = get_permalink($post); $headline = get_the_title($post); $u = get_userdata($post->post_author); $obj = array( '@context' => 'https://schema.org', '@type' => 'Article', 'url' => $url, 'headline' => wp_strip_all_tags($headline), 'publisher' => array('@type' => 'Organization', 'name' => $site), ); if ($u && $u->display_name) { $obj['author'] = array('@type' => 'Person', 'name' => $u->display_name); } $flags = 0; if (defined('JSON_UNESCAPED_UNICODE')) { $flags |= JSON_UNESCAPED_UNICODE; } if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } return function_exists('wp_json_encode') ? wp_json_encode($obj, $flags) : json_encode($obj); } /* ===== Export ===== */ private function ensure_dir() { $u = wp_upload_dir(); if (empty($u['basedir'])) { return false; } $dir = trailingslashit($u['basedir']) . self::EXPORT_DIR; if (!file_exists($dir)) { wp_mkdir_p($dir); } return (is_dir($dir) && is_writable($dir)) ? $dir : false; } private function build_query($a) { $q = array( 'post_type' => !empty($a['post_types']) ? $a['post_types'] : array('post'), 'posts_per_page' => -1, 'post_status' => 'publish', ); if (!empty($a['s'])) { $q['s'] = $a['s']; } if (!empty($a['author'])) { $q['author'] = (int) $a['author']; } if (!empty($a['cat'])) { $q['cat'] = (int) $a['cat']; } $date_query = array(); if (!empty($a['date_from'])) { $date_query[] = array('after' => $a['date_from']); } if (!empty($a['date_to'])) { $date_query[] = array('before' => $a['date_to']); } if ($date_query) { $q['date_query'] = $date_query; } return $q; } private function run_export($args) { $dir = $this->ensure_dir(); if (!$dir) { return array('ok' => false, 'file' => '', 'count' => 0, 'size_kb' => 0); } $q = new WP_Query($this->build_query($args)); $flags = 0; if (defined('JSON_UNESCAPED_UNICODE')) { $flags |= JSON_UNESCAPED_UNICODE; } if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } if (empty($args['minify']) && defined('JSON_PRETTY_PRINT')) { $flags |= JSON_PRETTY_PRINT; } $count = 0; $files = array(); if ($q->have_posts()) { while ($q->have_posts()) { $q->the_post(); $p = get_post(); $payload = $this->extract_payload($p); if (empty($args['include_acf'])) { unset($payload['acf']); } $json = function_exists('wp_json_encode') ? wp_json_encode($payload, $flags) : json_encode($payload); $fn = 'post-' . $p->ID . '.json'; file_put_contents(trailingslashit($dir) . $fn, $json); $files[] = $fn; $count++; } wp_reset_postdata(); } $file_out = ''; $size_kb = 0; $stamp = date_i18n('Ymd-His'); if (!empty($args['zip']) && class_exists('ZipArchive')) { $zipname = 'ai-json-export-' . $stamp . '.zip'; $zip = new ZipArchive(); $zip_path = trailingslashit($dir) . $zipname; if (true === $zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { $size_total = 0; foreach ($files as $fn) { $path = trailingslashit($dir) . $fn; if (file_exists($path)) { $zip->addFile($path, $fn); $size_total += filesize($path); } } $zip->close(); $this->log_export($zipname, $count, round($size_total / 1024, 1), $args); $file_out = $zipname; $size_kb = round($size_total / 1024, 1); } } else { $outname = 'ai-json-export-' . $stamp . '.json'; $bundle = array(); foreach ($files as $fn) { $p = trailingslashit($dir) . $fn; if (file_exists($p)) { $bundle[] = json_decode(file_get_contents($p), true); } } $json_bundle = function_exists('wp_json_encode') ? wp_json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : json_encode($bundle); file_put_contents(trailingslashit($dir) . $outname, $json_bundle); $this->log_export($outname, $count, round(strlen($json_bundle) / 1024, 1), $args); $file_out = $outname; $size_kb = round(strlen($json_bundle) / 1024, 1); } return array('ok' => true, 'file' => $file_out, 'count' => $count, 'size_kb' => $size_kb); } private function log_export($filename, $count, $size_kb, $args) { $u = wp_upload_dir(); $dir = trailingslashit($u['basedir']) . self::EXPORT_DIR; $log = trailingslashit($dir) . self::LOG_FILE; if (!file_exists($dir)) { wp_mkdir_p($dir); } $filters = sprintf( 'types:%s | author:%s | cat:%s | date:%s..%s | search:%s | acf:%s | min:%s | zip:%s', implode(',', (array) $args['post_types']), !empty($args['author']) ? (int) $args['author'] : '-', !empty($args['cat']) ? (int) $args['cat'] : '-', !empty($args['date_from']) ? $args['date_from'] : '-', !empty($args['date_to']) ? $args['date_to'] : '-', !empty($args['s']) ? $args['s'] : '-', !empty($args['include_acf']) ? '1' : '0', !empty($args['minify']) ? '1' : '0', !empty($args['zip']) ? '1' : '0' ); $fh = fopen($log, file_exists($log) ? 'a' : 'w'); if ($fh) { fputcsv($fh, array(date_i18n('c'), $filename, $count, $size_kb, $filters)); fclose($fh); } } public function handle_export() { if (!current_user_can('manage_options')) { wp_die(esc_html__('No permission.', 'ai-json-extractor-auto-embed-lite')); } check_admin_referer('aije_export'); $a = array( 'post_types' => isset($_POST['post_types']) ? array_map('sanitize_key', (array) $_POST['post_types']) : array('post', 'page'), 'author' => isset($_POST['author']) ? (int) $_POST['author'] : 0, 'cat' => isset($_POST['cat']) ? (int) $_POST['cat'] : 0, 'date_from' => isset($_POST['date_from']) ? sanitize_text_field(wp_unslash($_POST['date_from'])) : '', 'date_to' => isset($_POST['date_to']) ? sanitize_text_field(wp_unslash($_POST['date_to'])) : '', 's' => isset($_POST['s']) ? sanitize_text_field(wp_unslash($_POST['s'])) : '', 'include_acf' => !empty($_POST['include_acf']) ? 1 : 0, 'minify' => !empty($_POST['minify']) ? 1 : 0, 'zip' => !empty($_POST['zip']) ? 1 : 0, ); update_option(self::LAST_ARGS_OPT, $a, false); $res = $this->run_export($a); wp_safe_redirect(add_query_arg(array( 'page' => 'aije-lite', 'tab' => 'export', 'export_done' => $res['ok'] ? 1 : 0, 'file' => $res['file'], 'count' => $res['count'], 'size_kb' => $res['size_kb'], ), admin_url('tools.php'))); exit; } public function ajax_ai_analyze() { check_ajax_referer('aije_ai_nonce', 'nonce'); if (!current_user_can('edit_posts')) { wp_send_json_error(); } $post_id = intval($_POST['post_id']); $post = get_post($post_id); if (!$post) { wp_send_json_error(); } $o = $this->get_opts(); $api_key = isset($o['openai_api_key']) ? $o['openai_api_key'] : ''; if (!$api_key) { wp_send_json_error(array('html' => '<p>Kein API Key hinterlegt.</p>')); } $content = wp_strip_all_tags($post->post_content); $result = $this->call_openai($content, $api_key); update_post_meta($post_id, '_aije_ai_analysis', $result); wp_send_json_success(array('html' => $this->render_ai_analysis($result))); } /* ===== Cron ===== */ public function add_schedules($s) { $s['aije_daily'] = array('interval' => DAY_IN_SECONDS, 'display' => 'AI JSON – täglich'); $s['aije_weekly'] = array('interval' => WEEK_IN_SECONDS, 'display' => 'AI JSON – wöchentlich'); $s['aije_15min'] = array('interval' => 900, 'display' => 'AI JSON – alle 15 Minuten'); return $s; } private function maybe_reschedule_export($enabled, $freq) { $t = wp_next_scheduled(self::CRON_EXPORT); if ($enabled) { if ($t) { wp_unschedule_event($t, self::CRON_EXPORT); } $key = ($freq === 'weekly') ? 'aije_weekly' : 'aije_daily'; $schedules = wp_get_schedules(); if (!isset($schedules[$key])) { $key = 'daily'; } wp_schedule_event(time() + 300, $key, self::CRON_EXPORT); } elseif ($t) { wp_unschedule_event($t, self::CRON_EXPORT); } } public function handle_run_cron_now() { if (!current_user_can('manage_options')) { wp_die(esc_html__('No permission.', 'ai-json-extractor-auto-embed-lite')); } check_admin_referer('aije_export'); $a = get_option(self::LAST_ARGS_OPT, array( 'post_types' => array('post', 'page'), 'author' => 0, 'cat' => 0, 'date_from' => '', 'date_to' => '', 's' => '', 'include_acf' => 1, 'minify' => 0, 'zip' => 1, )); $this->run_export($a); wp_safe_redirect(add_query_arg(array('page' => 'aije-lite', 'tab' => 'automation', 'ran' => 1), admin_url('tools.php'))); exit; } public function handle_clear_cron() { if (!current_user_can('manage_options')) { wp_die(esc_html__('No permission.', 'ai-json-extractor-auto-embed-lite')); } check_admin_referer('aije_export'); $t = wp_next_scheduled(self::CRON_EXPORT); if ($t) { wp_unschedule_event($t, self::CRON_EXPORT); } wp_safe_redirect(add_query_arg(array('page' => 'aije-lite', 'tab' => 'automation', 'cleared' => 1), admin_url('tools.php'))); exit; } public function cron_export_runner() { $o = $this->get_opts(); if (empty($o['cron_enabled'])) { return; } $a = get_option(self::LAST_ARGS_OPT, array( 'post_types' => array('post', 'page'), 'author' => 0, 'cat' => 0, 'date_from' => '', 'date_to' => '', 's' => '', 'include_acf' => 1, 'minify' => 0, 'zip' => 1, )); $this->run_export($a); } /* ===== PingHub ===== */ /* ===== Telemetrie-Einwilligung ===== */ /** * Zeigt eine einmalige Admin-Notice mit Opt-out-Möglichkeit. * Wird ausgeblendet sobald der Nutzer eine Entscheidung getroffen hat. */ public function stats_consent_notice() { // Bereits entschieden? if ( get_option( 'aije_stats_notice_dismissed' ) ) { return; } // Nur für Admins if ( ! current_user_can( 'manage_options' ) ) { return; } $is_optout = (bool) get_option( self::OPT_STATS_OPTOUT ); $optout_url = wp_nonce_url( admin_url( 'admin-post.php?action=aije_stats_optout' ), 'aije_stats_optout' ); $optin_url = wp_nonce_url( admin_url( 'admin-post.php?action=aije_stats_optin' ), 'aije_stats_optin' ); echo '<div class="notice notice-info" style="padding:14px 16px;display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;">'; echo '<div style="flex:1;min-width:280px;">'; echo '<strong>AI Auto Embed – Anonyme Nutzungsstatistiken</strong><br>'; echo '<span style="font-size:13px;color:#555;line-height:1.6;">'; echo 'Das Plugin sendet anonyme Daten an den Entwickler: Site-URL, PHP-Version und aggregierte Score-Durchschnitte (keine Inhalte, keine E-Mails, keine personenbezogenen Daten). '; echo 'Das hilft dabei, das Plugin zu verbessern. Du kannst das jederzeit deaktivieren.'; echo '</span>'; echo '</div>'; echo '<div style="display:flex;gap:8px;align-items:center;flex-shrink:0;padding-top:2px;">'; if ( $is_optout ) { echo '<span style="color:#888;font-size:12px;">Statistiken deaktiviert.</span> '; echo '<a href="' . esc_url( $optin_url ) . '" class="button button-small">Wieder aktivieren</a>'; } else { echo '<a href="' . esc_url( $optout_url ) . '" class="button button-small">Deaktivieren</a>'; echo '<a href="' . esc_url( $optin_url ) . '" class="button button-primary button-small">OK, verstanden</a>'; } echo '</div>'; echo '</div>'; } /** * Handler: Nutzer deaktiviert Telemetrie. */ public function handle_stats_optout() { check_admin_referer( 'aije_stats_optout' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } update_option( self::OPT_STATS_OPTOUT, 1, false ); update_option( 'aije_stats_notice_dismissed', 1, false ); wp_safe_redirect( add_query_arg( 'aije_stats', 'optout', wp_get_referer() ?: admin_url() ) ); exit; } /** * Handler: Nutzer akzeptiert / reaktiviert Telemetrie. */ public function handle_stats_optin() { check_admin_referer( 'aije_stats_optin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } delete_option( self::OPT_STATS_OPTOUT ); update_option( 'aije_stats_notice_dismissed', 1, false ); wp_safe_redirect( add_query_arg( 'aije_stats', 'optin', wp_get_referer() ?: admin_url() ) ); exit; } /* ===== Stats: Heartbeat & Score-Reporting ===== */ /** * Stellt sicher dass der wöchentliche Heartbeat-Cron läuft. */ private function ensure_heartbeat_cron() { $scheduled = wp_next_scheduled( self::CRON_HEARTBEAT ); // Auf täglich umstellen falls noch wöchentlich if ( $scheduled ) { $schedule = wp_get_schedule( self::CRON_HEARTBEAT ); if ( $schedule === 'aije_weekly' ) { wp_unschedule_event( $scheduled, self::CRON_HEARTBEAT ); $scheduled = false; } } if ( ! $scheduled ) { wp_schedule_event( time() + 300, 'aije_daily', self::CRON_HEARTBEAT ); } } /** * Wöchentlicher Heartbeat: sendet anonyme Installations-Daten. */ /** * Berechnet einen installations-spezifischen Secret-Hash. * Salt + Site-URL → SHA-256. Kein fixer Secret im Code. */ public static function stats_secret() { return hash( 'sha256', home_url() . self::STATS_SALT ); } public function cron_heartbeat_runner() { // Opt-out prüfen: Nutzer hat Telemetrie deaktiviert if ( get_option( self::OPT_STATS_OPTOUT ) ) { return; } global $wp_version; $o = $this->get_opts(); // Beitrags-Anzahl zählen $post_count = 0; foreach ( $o['enabled_post_types'] as $pt ) { $count = wp_count_posts( $pt ); $post_count += (int) ( $count->publish ?? 0 ); } $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'wp_version' => $wp_version ?? '', 'php_version' => PHP_VERSION, 'plugin_version' => AIJE_PLUGIN_VERSION, 'post_count' => $post_count, ); wp_remote_post( self::STATS_ENDPOINT . 'heartbeat', array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'blocking' => false, // Nicht auf Antwort warten 'sslverify' => true, ) ); // Zeitstempel des letzten Heartbeats speichern update_option( 'aije_last_heartbeat', time(), false ); // Direkt danach Score-Statistiken senden $this->send_score_stats(); } /** * Sendet anonyme Score-Statistiken. * Kein Inhalt, keine URLs – nur aggregierte Zahlen. */ private function send_score_stats() { $o = $this->get_opts(); $types = $o['enabled_post_types']; $query = new WP_Query( array( 'post_type' => $types, 'posts_per_page' => -1, 'post_status' => 'publish', 'no_found_rows' => true, ) ); if ( ! $query->have_posts() ) { return; } $scores = array(); $cat_totals = array( 'content' => 0, 'eeat' => 0, 'technical' => 0, 'linking' => 0 ); $issue_freq = array(); $count = 0; while ( $query->have_posts() ) { $query->the_post(); $analysis = $this->calculate_ai_analysis( get_post() ); $scores[] = $analysis['score']; foreach ( $cat_totals as $k => $_ ) { $cat_totals[ $k ] += $analysis['categories'][ $k ]['score'] ?? 0; } // Häufigste Issues zählen (nur Message, kein Inhalt) foreach ( array_slice( $analysis['issues'], 0, 3 ) as $issue ) { $key = $issue['msg'] ?? ''; $issue_freq[ $key ] = ( $issue_freq[ $key ] ?? 0 ) + 1; } $count++; } wp_reset_postdata(); if ( $count === 0 ) { return; } arsort( $issue_freq ); $avg_score = round( array_sum( $scores ) / $count ); $cat_avgs = array(); foreach ( $cat_totals as $k => $total ) { $cat_avgs[ $k ] = round( $total / $count ); } $payload = array( 'secret' => self::stats_secret(), 'site_url' => home_url(), 'plugin_version' => AIJE_PLUGIN_VERSION, 'preset' => $o['score_preset'] ?? 'standard', 'avg_score' => $avg_score, 'scores' => $cat_avgs, 'top_issues' => array_keys( array_slice( $issue_freq, 0, 5, true ) ), 'post_count' => $count, ); wp_remote_post( self::STATS_ENDPOINT . 'score', array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'blocking' => false, 'sslverify' => true, ) ); } private function ensure_ping_cron() { $t = wp_next_scheduled(self::CRON_PING); if (!$t) { wp_schedule_event(time() + 600, 'aije_15min', self::CRON_PING); } } public function handle_test_ping() { if (!current_user_can('manage_options')) { wp_die(esc_html__('No permission.', 'ai-json-extractor-auto-embed-lite')); } check_admin_referer('aije_export'); $post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0; $event = isset($_POST['event']) ? sanitize_key($_POST['event']) : 'update'; if ($post_id > 0) { $this->dispatch_ping($event, $post_id); } wp_safe_redirect(add_query_arg(array('page' => 'aije-lite', 'tab' => 'pinghub', 'test' => 1), admin_url('tools.php'))); exit; } public function handle_flush_queue() { if (!current_user_can('manage_options')) { wp_die(esc_html__('No permission.', 'ai-json-extractor-auto-embed-lite')); } check_admin_referer('aije_export'); $this->cron_ping_runner(); wp_safe_redirect(add_query_arg(array('page' => 'aije-lite', 'tab' => 'pinghub', 'flushed' => 1), admin_url('tools.php'))); exit; } public function on_transition_status($new, $old, $post) { $o = $this->get_opts(); if (empty($o['ping_enabled'])) { return; } if (!in_array($post->post_type, $o['enabled_post_types'], true)) { return; } if ($new === 'publish' && $old !== 'publish' && in_array('publish', $o['ping_events'], true)) { $this->dispatch_ping('publish', $post->ID); } } public function on_post_updated($post_ID, $after, $before) { $o = $this->get_opts(); if (empty($o['ping_enabled'])) { return; } if (!in_array($after->post_type, $o['enabled_post_types'], true)) { return; } if ($after->post_status !== 'publish') { return; } if (in_array('update', $o['ping_events'], true)) { $this->dispatch_ping('update', $post_ID); } } private function push_ping_error($url, $event, $post_id, $msg) { $errs = get_option(self::PING_ERRORS_OPT, array()); if (!is_array($errs)) { $errs = array(); } $errs[] = array('time' => date_i18n('Y-m-d H:i:s'), 'url' => $url, 'event' => $event, 'post_id' => $post_id, 'error' => $msg); if (count($errs) > 200) { $errs = array_slice($errs, -200); } update_option(self::PING_ERRORS_OPT, $errs, false); } private function dispatch_ping($event, $post_id) { $o = $this->get_opts(); $urls = preg_split("/\r\n|\n|\r/", trim($o['ping_urls'])); $urls = array_filter(array_map('trim', $urls)); if (empty($urls)) { return; } $post_obj = get_post($post_id); if (!$post_obj) { return; } $payload = array( 'event' => $event, 'site' => get_bloginfo('name'), 'time' => time(), 'post' => $this->extract_payload($post_obj), ); $json = function_exists('wp_json_encode') ? wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : json_encode($payload); $headers = array('Content-Type' => 'application/json'); if (!empty($o['ping_secret'])) { $headers['X-AIJE-Signature'] = 'sha256=' . hash_hmac('sha256', $json, $o['ping_secret']); } foreach ($urls as $u) { if ($u === '' || substr($u, 0, 1) === '#') { continue; } $resp = wp_remote_post($u, array( 'method' => 'POST', 'timeout' => $o['ping_timeout'], 'headers' => $headers, 'body' => $json, 'blocking' => true, 'data_format' => 'body', )); if (is_wp_error($resp)) { $this->push_ping_error($u, $event, $post_id, $resp->get_error_message()); if (!empty($o['ping_retry'])) { $this->queue_ping($u, $json, $headers); } } else { $code = wp_remote_retrieve_response_code($resp); if ($code < 200 || $code >= 300) { $this->push_ping_error($u, $event, $post_id, 'HTTP ' . $code); if (!empty($o['ping_retry'])) { $this->queue_ping($u, $json, $headers); } } } } } private function queue_ping($url, $json, $headers) { $q = get_option(self::PING_QUEUE_OPT, array()); if (!is_array($q)) { $q = array(); } $q[] = array('url' => $url, 'payload' => $json, 'headers' => $headers, 'tries' => 0, 'last' => time()); update_option(self::PING_QUEUE_OPT, $q, false); $this->ensure_ping_cron(); } public function cron_ping_runner() { $q = get_option(self::PING_QUEUE_OPT, array()); if (empty($q) || !is_array($q)) { return; } $o = $this->get_opts(); $next = array(); foreach ($q as $job) { $resp = wp_remote_post($job['url'], array( 'method' => 'POST', 'timeout' => $o['ping_timeout'], 'headers' => $job['headers'], 'body' => $job['payload'], 'blocking' => true, 'data_format' => 'body', )); if (is_wp_error($resp)) { $job['tries'] = (int) $job['tries'] + 1; $this->push_ping_error($job['url'], 'retry', 0, $resp->get_error_message()); if ($job['tries'] < 3) { $job['last'] = time(); $next[] = $job; } } else { $code = wp_remote_retrieve_response_code($resp); if ($code < 200 || $code >= 300) { $job['tries'] = (int) $job['tries'] + 1; $this->push_ping_error($job['url'], 'retry', 0, 'HTTP ' . $code); if ($job['tries'] < 3) { $job['last'] = time(); $next[] = $job; } } } } update_option(self::PING_QUEUE_OPT, $next, false); } /* ===== Lizenz-Logik ===== */ private static function get_normalized_host() { $home = function_exists('home_url') ? home_url() : ''; $site = function_exists('site_url') ? site_url() : ''; $url = $home ? $home : $site; $host = ''; if ($url) { $parsed = @parse_url($url); if (is_array($parsed) && !empty($parsed['host'])) { $host = strtolower($parsed['host']); } } if ($host !== '' && strpos($host, 'www.') === 0) { $host = substr($host, 4); } return $host; } private static function base_domain($host) { if (!$host) { return ''; } $parts = explode('.', $host); $n = count($parts); if ($n >= 2) { return $parts[$n - 2] . '.' . $parts[$n - 1]; } return $host; } private static function code_from_domain($domain) { $domain = strtolower((string) $domain); if ($domain === '') { return ''; } $hex = hash('sha256', $domain); return strtoupper(substr($hex, 0, 16)); } public static function license_is_valid($code = null) { $entered = strtoupper(trim((string) ($code ? $code : get_option(self::OPT_LICENSE, '')))); if ($entered === '') { return false; } $host = self::get_normalized_host(); if ($host === '') { return false; } $code1 = self::code_from_domain($host); if ($code1 && hash_equals($code1, $entered)) { return true; } $base = self::base_domain($host); if ($base && $base !== $host) { $code2 = self::code_from_domain($base); if ($code2 && hash_equals($code2, $entered)) { return true; } } return false; } } endif; /* ===== Bootstrap ===== */ new AIJE_Lite_100(); /** * Globaler Helper – bleibt als Hookpunkt erhalten. * Gibt aktuell immer true zurück (alle Features freigeschaltet). * Kann für künftige Pro-Features reaktiviert werden. */ if (!function_exists('aije_pro_enabled')) { function aije_pro_enabled() { return true; } }