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; } 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 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 ); if ( $expiry && time() > $expiry - 300 ) { $new_token = $this->refresh_gsc_token(); if ( $new_token ) { return $new_token; } delete_option( 'aije_gsc_access_token' ); return ''; } return $access_token; } /** * Ruft GSC Search Analytics ab und gibt Rohdaten zurück. * Wird von tab-gsc.php (Seitenansicht) und render_gsc_data() genutzt. */ private function gsc_query( $access_token, $property, $body_array, $timeout = 15 ) { $resp = wp_remote_post( 'https://www.googleapis.com/webmasters/v3/sites/' . urlencode( $property ) . '/searchAnalytics/query', array( 'headers' => array( 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body_array ), 'timeout' => $timeout ) ); if ( is_wp_error( $resp ) ) { return array(); } return json_decode( wp_remote_retrieve_body( $resp ), true )['rows'] ?? array(); } private function gsc_get_sites( $access_token ) { $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 ) ) { return array(); } if ( wp_remote_retrieve_response_code( $resp ) === 401 ) { $new = $this->refresh_gsc_token(); if ( ! $new ) { delete_option( 'aije_gsc_access_token' ); return array(); } $resp = wp_remote_get( 'https://www.googleapis.com/webmasters/v3/sites', array( 'headers' => array( 'Authorization' => 'Bearer ' . $new ), 'timeout' => 10 ) ); if ( is_wp_error( $resp ) ) { return array(); } } return json_decode( wp_remote_retrieve_body( $resp ), true )['siteEntry'] ?? array(); } /** * Holt query+page Rohdaten für die Opportunity-Berechnungen, gecacht via Transient * (Standard-Cache-Dauer 6h, damit GSC-API nicht bei jedem Tab-Aufruf neu abgefragt wird). */ private function gsc_get_query_page_rows( $access_token, $property, $start, $end ) { $cache_key = 'aije_gsc_qp_' . md5( $property . $start . $end ); $cached = get_transient( $cache_key ); if ( $cached !== false ) { return $cached; } $rows = $this->gsc_query( $access_token, $property, array( 'startDate' => $start, 'endDate' => $end, 'dimensions' => array( 'query', 'page' ), 'rowLimit' => 5000, 'orderBy' => array( array( 'fieldName' => 'impressions', 'sortOrder' => 'DESCENDING' ) ), ), 25 ); set_transient( $cache_key, $rows, 6 * HOUR_IN_SECONDS ); return $rows; } /** * Branchenstandard-Erwartungs-CTR-Kurve nach Position (gerundete Richtwerte). * Quelle: aggregierte Industry-Benchmarks (Backlinko/Advanced Web Ranking-Größenordnung). */ private function gsc_expected_ctr( $position ) { $curve = array( 1 => 28.0, 2 => 15.0, 3 => 11.0, 4 => 8.0, 5 => 6.0, 6 => 5.0, 7 => 4.0, 8 => 3.0, 9 => 2.5, 10 => 2.0, ); $p = max( 1, min( 10, round( $position ) ) ); if ( $position > 10 ) { return 1.0; } return $curve[ $p ] ?? 2.0; } /** * Zentrale Gate-Funktion für das Opportunity Radar. * Aktuell: immer true (Free). Später: hier Tier-Check einbauen (z. B. aije_pro_enabled()). */ private function gsc_opportunity_radar_enabled() { return true; } /** * Berechnet die drei Opportunity-Kategorien aus query+page Rohdaten. * Gibt array mit 'striking_distance', 'ctr_leaks', 'cannibalization' zurück. */ private function gsc_compute_opportunities( $rows ) { $striking = array(); $ctr_leaks = array(); $by_query = array(); foreach ( $rows as $r ) { $query = $r['keys'][0] ?? ''; $page = $r['keys'][1] ?? ''; if ( $query === '' || $page === '' ) { continue; } $clicks = (int) ( $r['clicks'] ?? 0 ); $impr = (int) ( $r['impressions'] ?? 0 ); $ctr = round( ( $r['ctr'] ?? 0 ) * 100, 2 ); $pos = round( $r['position'] ?? 0, 1 ); // 1) Striking Distance: Position 5–15, Mindest-Impressions, um Rauschen zu filtern if ( $pos >= 5 && $pos <= 15 && $impr >= 20 ) { $striking[] = array( 'query' => $query, 'page' => $page, 'position' => $pos, 'impressions' => $impr, 'clicks' => $clicks, 'ctr' => $ctr, ); } // 2) CTR-Lecks: tatsächliche CTR deutlich unter Erwartungswert für die Position if ( $impr >= 50 && $pos <= 20 ) { $expected = $this->gsc_expected_ctr( $pos ); $gap = $expected - $ctr; // positiver Wert = Leck if ( $gap >= 1.5 ) { $ctr_leaks[] = array( 'query' => $query, 'page' => $page, 'position' => $pos, 'impressions' => $impr, 'ctr' => $ctr, 'expected_ctr' => $expected, 'gap' => round( $gap, 1 ), 'lost_clicks_est' => (int) round( $impr * ( $gap / 100 ) ), ); } } // 3) Kannibalisierung: gruppiere nach Query, sammle alle Pages if ( ! isset( $by_query[ $query ] ) ) { $by_query[ $query ] = array(); } $by_query[ $query ][] = array( 'page' => $page, 'impressions' => $impr, 'clicks' => $clicks, 'position' => $pos ); } $cannibalization = array(); foreach ( $by_query as $query => $pages ) { if ( count( $pages ) < 2 ) { continue; } // Nur relevant, wenn mehr als eine Seite nennenswerte Impressionen hat $relevant = array_filter( $pages, function( $p ) { return $p['impressions'] >= 10; } ); if ( count( $relevant ) < 2 ) { continue; } usort( $relevant, function( $a, $b ) { return $b['impressions'] - $a['impressions']; } ); $total_impr = array_sum( array_column( $relevant, 'impressions' ) ); $cannibalization[] = array( 'query' => $query, 'pages' => $relevant, 'page_count' => count( $relevant ), 'total_impressions' => $total_impr, ); } usort( $cannibalization, function( $a, $b ) { return $b['total_impressions'] - $a['total_impressions']; } ); usort( $striking, function( $a, $b ) { return $b['impressions'] - $a['impressions']; } ); usort( $ctr_leaks, function( $a, $b ) { return $b['lost_clicks_est'] - $a['lost_clicks_est']; } ); return array( 'striking_distance' => $striking, 'ctr_leaks' => $ctr_leaks, 'cannibalization' => $cannibalization, 'counts' => array( 'striking_distance' => count( $striking ), 'ctr_leaks' => count( $ctr_leaks ), 'cannibalization' => count( $cannibalization ), ), ); } /** * Generisches Suchfeld + sortierbare Tabelle (gleiches Bedienkonzept wie der Seiten-Tab). * $table_id muss pro Tabelle eindeutig sein, $columns = array( array(key,label,align,sortable) ). * $row_renderer ist eine Closure, die für jede Datenzeile ... als String zurückgibt * und dabei data-search auf dem setzt (für die Filterung). */ private function render_sortable_table( $table_id, $columns, $rows_data, $row_renderer, $default_sort_col = null ) { echo ''; echo '
'; echo ''; foreach ( $columns as $col ) { list( $key, $label, $align, $sortable ) = $col; $onclick = $sortable ? ' onclick="aije_tbl_sort(\'' . esc_js( $table_id ) . '\',\'' . esc_js( $key ) . '\',this)"' : ''; $cursor = $sortable ? 'cursor:pointer;user-select:none;' : ''; echo ''; } echo ''; foreach ( $rows_data as $row ) { echo $row_renderer( $row ); } echo '
' . esc_html( $label ) . ( $sortable ? ' ↕' : '' ) . '
'; if ( $default_sort_col ) { echo ''; } } /** * Gibt das gemeinsame JS für Sortierung/Filterung aller generischen Tabellen aus. * Wird einmal pro Seitenaufruf benötigt — aufgerufen am Ende von render_gsc_data(). */ private function render_sortable_table_script() { ?> gsc_get_query_page_rows( $access_token, $property, $start, $end ); if ( empty( $rows ) ) { echo '

' . esc_html__( 'Noch keine ausreichenden Suchdaten im gewählten Zeitraum.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $opp = $this->gsc_compute_opportunities( $rows ); $data = $opp['striking_distance']; echo '

🚀 ' . sprintf( esc_html__( 'Keywords auf Position 5–15 mit Quick-Win-Potenzial Richtung Top 5 · %d gefunden', 'ai-json-extractor-auto-embed-lite' ), count( $data ) ) . '

'; if ( empty( $data ) ) { echo '

' . esc_html__( 'Keine Quick-Win-Keywords im aktuellen Zeitraum.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $columns = array( array( 'query', __( 'Suchbegriff', 'ai-json-extractor-auto-embed-lite' ), 'left', true ), array( 'page', __( 'Seite', 'ai-json-extractor-auto-embed-lite' ), 'left', true ), array( 'position', __( 'Position', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'impressions', __( 'Impressionen', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'clicks', __( 'Klicks', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'ctr', __( 'CTR', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), ); $path = function( $url ) { return str_replace( home_url(), '', $url ) ?: '/'; }; $renderer = function( $r ) use ( $path ) { $p = $path( $r['page'] ); $search = strtolower( $r['query'] . ' ' . $p ); $html = ''; $html .= '' . esc_html( $r['query'] ) . ''; $html .= '' . esc_html( strlen( $p ) > 35 ? substr( $p, 0, 32 ) . '…' : $p ) . ''; $html .= '' . esc_html( $r['position'] ) . ''; $html .= '' . esc_html( number_format_i18n( $r['impressions'] ) ) . ''; $html .= '' . esc_html( number_format_i18n( $r['clicks'] ) ) . ''; $html .= '' . esc_html( $r['ctr'] ) . '%'; $html .= ''; return $html; }; $this->render_sortable_table( 'aije-gsc-striking', $columns, $data, $renderer, 'impressions' ); } /** * Tab "CTR-Lecks": vollständige sortierbare Tabelle. */ private function render_gsc_ctr_leaks_tab( $access_token, $property, $start, $end ) { $rows = $this->gsc_get_query_page_rows( $access_token, $property, $start, $end ); if ( empty( $rows ) ) { echo '

' . esc_html__( 'Noch keine ausreichenden Suchdaten im gewählten Zeitraum.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $opp = $this->gsc_compute_opportunities( $rows ); $data = $opp['ctr_leaks']; echo '

📉 ' . sprintf( esc_html__( 'Suchbegriffe, deren CTR deutlich unter dem Erwartungswert für ihre Position liegt · %d gefunden', 'ai-json-extractor-auto-embed-lite' ), count( $data ) ) . '

'; if ( empty( $data ) ) { echo '

' . esc_html__( 'Keine auffälligen CTR-Lecks gefunden.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $columns = array( array( 'query', __( 'Suchbegriff', 'ai-json-extractor-auto-embed-lite' ), 'left', true ), array( 'page', __( 'Seite', 'ai-json-extractor-auto-embed-lite' ), 'left', true ), array( 'position', __( 'Position', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'impressions', __( 'Impressionen', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'ctr', __( 'CTR (ist)', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'expected_ctr', __( 'CTR (erwartet)', 'ai-json-extractor-auto-embed-lite' ), 'right', true ), array( 'lost_clicks_est', 'Verpasste Klicks (≈)', 'right', true ), ); $path = function( $url ) { return str_replace( home_url(), '', $url ) ?: '/'; }; $renderer = function( $r ) use ( $path ) { $p = $path( $r['page'] ); $search = strtolower( $r['query'] . ' ' . $p ); $html = ''; $html .= '' . esc_html( $r['query'] ) . ''; $html .= '' . esc_html( strlen( $p ) > 35 ? substr( $p, 0, 32 ) . '…' : $p ) . ''; $html .= '' . esc_html( $r['position'] ) . ''; $html .= '' . esc_html( number_format_i18n( $r['impressions'] ) ) . ''; $html .= '' . esc_html( $r['ctr'] ) . '%'; $html .= '' . esc_html( $r['expected_ctr'] ) . '%'; $html .= '~' . esc_html( number_format_i18n( $r['lost_clicks_est'] ) ) . ''; $html .= ''; return $html; }; $this->render_sortable_table( 'aije-gsc-ctrleaks', $columns, $data, $renderer, 'lost_clicks_est' ); } /** * Tab "Keyword-Kannibalisierung": eine Hauptzeile pro Suchbegriff, aufklappbar zu den konkurrierenden URLs. */ private function render_gsc_cannibalization_tab( $access_token, $property, $start, $end ) { $rows = $this->gsc_get_query_page_rows( $access_token, $property, $start, $end ); if ( empty( $rows ) ) { echo '

' . esc_html__( 'Noch keine ausreichenden Suchdaten im gewählten Zeitraum.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $opp = $this->gsc_compute_opportunities( $rows ); $data = $opp['cannibalization']; echo '

⚔️ ' . sprintf( esc_html__( 'Suchbegriffe, für die mehrere eigene Seiten gleichzeitig Impressionen erhalten · %d betroffene Suchbegriffe · Klick auf eine Zeile zeigt die einzelnen URLs', 'ai-json-extractor-auto-embed-lite' ), count( $data ) ) . '

'; if ( empty( $data ) ) { echo '

' . esc_html__( 'Keine Kannibalisierung erkannt.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $path = function( $url ) { return str_replace( home_url(), '', $url ) ?: '/'; }; $table_id = 'aije-gsc-cannib'; echo ''; echo '
'; echo ''; foreach ( array( array( 'query', __( 'Suchbegriff', 'ai-json-extractor-auto-embed-lite' ), 'left' ), array( 'page_count', __( 'Seiten', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'impressions', __( 'Impressionen (gesamt)', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'clicks', __( 'Klicks (gesamt)', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( '', '', 'center' ), ) as $col ) { $onclick = $col[0] !== '' ? ' onclick="aije_cannib_sort(\'' . esc_js( $col[0] ) . '\',this)"' : ''; $cursor = $col[0] !== '' ? 'cursor:pointer;user-select:none;' : ''; echo ''; } echo ''; foreach ( $data as $i => $can ) { $total_clicks = array_sum( array_column( $can['pages'], 'clicks' ) ); $row_id = $table_id . '-row-' . $i; $search = strtolower( $can['query'] ); // Hauptzeile echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; // Detailzeile (initial versteckt) mit den einzelnen URLs echo ''; echo ''; } echo '
' . esc_html( $col[1] ) . ( $col[0] !== '' ? ' ↕' : '' ) . '
▸' . esc_html( $can['query'] ) . '' . esc_html( $can['page_count'] ) . '' . esc_html( number_format_i18n( $can['total_impressions'] ) ) . '' . esc_html( number_format_i18n( $total_clicks ) ) . '' . esc_html__( 'Details', 'ai-json-extractor-auto-embed-lite' ) . '
'; ?> '; echo '
✨

Seiten in KI-Antworten (AI Overviews / AI Mode)

'; echo '

' . wp_kses( sprintf( __( 'Google zeigt seit dem 3. Juni 2026 in der Search Console, wie oft deine Seiten in %1$s und %2$s erscheinen. Diese Daten umfassen aktuell nur %3$s — keine Klicks oder Suchbegriffe.', 'ai-json-extractor-auto-embed-lite' ), 'AI Overviews', 'AI Mode', '' . esc_html__( 'Impressionen', 'ai-json-extractor-auto-embed-lite' ) . '' ), array( 'strong' => array() ) ) . '

'; echo '

' . wp_kses( sprintf( __( 'Google hat diese Daten bisher %s für die Search Console API freigegeben. WP Rankify kann sie deshalb noch nicht automatisch abrufen und in dieser Tabelle anzeigen — das ist eine aktuelle Einschränkung von Google selbst, nicht von diesem Plugin. Sobald Google die API erweitert, wird dieser Tab automatisch mit echten Daten befüllt.', 'ai-json-extractor-auto-embed-lite' ), '' . esc_html__( 'nicht', 'ai-json-extractor-auto-embed-lite' ) . '' ), array( 'strong' => array() ) ) . '

'; echo '🔗 In der echten Search Console prüfen'; echo '

' . esc_html__( 'Dort: Performance-Bericht öffnen → Filter „Suchtyp" bzw. „Search Appearance" → AI Overviews / AI Mode auswählen.', 'ai-json-extractor-auto-embed-lite' ) . '

'; echo ''; } /** * Rendert die GSC-Datentabelle (aufgerufen aus tab-gsc.php wenn verbunden). */ private function render_gsc_data( $access_token ) { $access_token = $this->get_valid_gsc_token(); if ( ! $access_token ) { echo '

⚠ Google Token abgelaufen. Bitte Verbindung trennen und neu verbinden.

'; return; } $entries = $this->gsc_get_sites( $access_token ); if ( empty( $entries ) ) { echo '

' . esc_html__( 'Keine Search Console Properties gefunden.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } $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 '
'; echo ''; $form_url = add_query_arg( array_filter( array( 'page' => 'aije-lite', 'tab' => 'gsc', 'gsc_days' => $_GET['gsc_days'] ?? null, 'gsc_subtab' => $_GET['gsc_subtab'] ?? null, 'gsc_sort' => $_GET['gsc_sort'] ?? null, 'gsc_dir' => $_GET['gsc_dir'] ?? null ) ), admin_url( 'tools.php' ) ); echo '
'; $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; $end = date( 'Y-m-d', strtotime( '-2 days' ) ); $start = date( 'Y-m-d', strtotime( '-' . ( $days + 2 ) . ' days' ) ); $cmp_end = date( 'Y-m-d', strtotime( '-' . ( $days + 2 ) . ' days' ) ); $cmp_start = date( 'Y-m-d', strtotime( '-' . ( $days * 2 + 2 ) . ' days' ) ); // Zeitraum-Umschalter (gilt für alle Sub-Tabs gemeinsam) $subtab = isset( $_GET['gsc_subtab'] ) ? sanitize_key( $_GET['gsc_subtab'] ) : 'overview'; $valid_subtabs = array( 'overview', 'striking', 'ctr_leaks', 'cannibalization', 'ai_appearances' ); if ( ! in_array( $subtab, $valid_subtabs, true ) ) { $subtab = 'overview'; } echo '
Zeitraum:'; 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_subtab' => $subtab, 'gsc_sort' => $_GET['gsc_sort'] ?? 'clicks', 'gsc_dir' => $_GET['gsc_dir'] ?? 'desc' ), admin_url( 'tools.php?page=aije-lite&tab=gsc' ) ); echo '' . esc_html( $label ) . ''; } echo '' . esc_html( $start ) . ' – ' . esc_html( $end ) . '
'; // --- Sub-Tab-Leiste --- $subtabs_def = array( 'overview' => '🔎 Darstellung in der Suche', 'striking' => '🚀 Quick Wins', 'ctr_leaks' => '📉 CTR-Lecks', 'cannibalization' => '⚔️ Kannibalisierung', 'ai_appearances' => '✨ KI-Antworten', ); echo '
'; foreach ( $subtabs_def as $key => $label ) { $is_active = ( $subtab === $key ); $tab_url = add_query_arg( array( 'gsc_subtab' => $key, 'gsc_days' => $days ), admin_url( 'tools.php?page=aije-lite&tab=gsc' ) ); $style = $is_active ? 'color:#f29400;border-bottom:2px solid #f29400;font-weight:700;' : 'color:#888;border-bottom:2px solid transparent;font-weight:600;'; echo '' . esc_html( $label ) . ''; } echo '
'; if ( $subtab === 'striking' ) { $this->render_gsc_striking_distance_tab( $access_token, $selected, $start, $end ); $this->render_sortable_table_script(); return; } if ( $subtab === 'ctr_leaks' ) { $this->render_gsc_ctr_leaks_tab( $access_token, $selected, $start, $end ); $this->render_sortable_table_script(); return; } if ( $subtab === 'cannibalization' ) { $this->render_gsc_cannibalization_tab( $access_token, $selected, $start, $end ); return; } if ( $subtab === 'ai_appearances' ) { $this->render_gsc_ai_appearances_tab( $selected ); return; } // --- Sub-Tab "overview": bestehende Darstellung in der Suche --- $rows = $this->gsc_query( $access_token, $selected, array( 'startDate' => $start, 'endDate' => $end, 'dimensions' => array( 'page' ), 'rowLimit' => 500, 'orderBy' => array( array( 'fieldName' => 'clicks', 'sortOrder' => 'DESCENDING' ) ) ) ); $cmp_rows = $this->gsc_query( $access_token, $selected, array( 'startDate' => $cmp_start, 'endDate' => $cmp_end, 'dimensions' => array( 'page' ), 'rowLimit' => 500 ) ); $cmp_index = array(); foreach ( $cmp_rows as $cr ) { $cmp_index[ $cr['keys'][0] ] = $cr; } echo '
' . count( $rows ) . ' Seiten im Zeitraum
'; if ( empty( $rows ) ) { echo '

' . esc_html__( 'Keine Daten gefunden.', 'ai-json-extractor-auto-embed-lite' ) . '

'; return; } // KPI-Zeile $total_clicks = array_sum( array_column( $rows, 'clicks' ) ); $total_impr = array_sum( array_column( $rows, 'impressions' ) ); $cmp_clicks = array_sum( array_column( $cmp_rows, 'clicks' ) ); $cmp_impr = array_sum( array_column( $cmp_rows, 'impressions' ) ); $ti_avg = $total_impr > 0 ? $total_impr : 1; $w_ctr = 0; $w_pos = 0; foreach ( $rows as $r ) { $iw = ( $r['impressions'] ?? 0 ) / $ti_avg; $w_ctr += ( $r['ctr'] ?? 0 ) * $iw; $w_pos += ( $r['position'] ?? 0 ) * $iw; } $avg_ctr = round( $w_ctr * 100, 1 ); $avg_pos = round( $w_pos, 1 ); $dc = $total_clicks - $cmp_clicks; $di = $total_impr - $cmp_impr; $fmt = function( $v, $inv = false ) { if ( $v == 0 ) return '±0'; $pos = $inv ? $v < 0 : $v > 0; $c = $pos ? '#22c55e' : '#ef4444'; return '' . ( $v > 0 ? '+' : '' ) . round( $v, 1 ) . ''; }; echo '
'; foreach ( array( array( __( 'Klicks', 'ai-json-extractor-auto-embed-lite' ), number_format_i18n( $total_clicks ), $fmt( $dc ), '' ), array( __( 'Impressionen', 'ai-json-extractor-auto-embed-lite' ), number_format_i18n( $total_impr ), $fmt( $di ), '' ), array( __( 'Ø CTR', 'ai-json-extractor-auto-embed-lite' ), $avg_ctr . '%', '', '' ), array( __( 'Ø Position', 'ai-json-extractor-auto-embed-lite' ), $avg_pos, '', '' ) ) as $s ) { echo '
' . esc_html( $s[0] ) . '
' . esc_html( $s[1] ) . '
' . ( $s[2] ? '
vs. Vorperiode: ' . $s[2] . '
' : '' ) . '
'; } echo '
'; // SEO-Issues aus Post-Daten $o = $this->get_opts(); $all_posts = get_posts( array( 'post_type' => $o['enabled_post_types'] ?? array( 'post' ), 'posts_per_page' => -1, 'post_status' => 'publish' ) ); $url_issues = array(); foreach ( $all_posts as $p ) { $issues = array(); if ( ! has_post_thumbnail( $p->ID ) ) { $issues[] = array( 'msg' => 'Kein Beitragsbild', 'severity' => 'medium' ); } $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' ); } if ( str_word_count( wp_strip_all_tags( $p->post_content ) ) < 300 ) { $issues[] = array( 'msg' => 'Text zu kurz', 'severity' => 'medium' ); } if ( ! preg_match( '/

post_content ) ) { $issues[] = array( 'msg' => 'Kein H2', 'severity' => 'medium' ); } if ( empty( get_user_meta( $p->post_author, 'aije_job_title', true ) ) ) { $issues[] = array( 'msg' => 'Kein E-E-A-T Profil', 'severity' => 'high' ); } if ( ! empty( $issues ) ) { $url_issues[ trailingslashit( get_permalink( $p->ID ) ) ] = $issues; } } // Suchfeld + Tabelle echo ''; echo '
'; echo ''; foreach ( array( array( 'page', __( 'Seite', 'ai-json-extractor-auto-embed-lite' ), 'left' ), array( 'tasks', __( 'Tasks', 'ai-json-extractor-auto-embed-lite' ), 'left' ), array( 'clicks', __( 'Klicks', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'clicks_d', '±', 'right' ), array( 'impr', __( 'Impressionen', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'ctr', __( 'CTR', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'pos', __( 'Ø Position', 'ai-json-extractor-auto-embed-lite' ), 'right' ), array( 'pos_d', '±', 'right' ) ) as $col ) { $no_sort = in_array( $col[0], array( 'tasks' ) ); $onclick = ! $no_sort ? ' onclick="aije_gsc_sort(\'' . esc_js( $col[0] ) . '\',this)"' : ''; $cursor = ! $no_sort ? 'cursor:pointer;user-select:none;' : ''; echo ''; } echo ''; foreach ( $rows as $row ) { $url = $row['keys'][0] ?? ''; $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' ); $cmp = $cmp_index[ $url ] ?? null; $d_clicks = $cmp ? $clicks - (int) ( $cmp['clicks'] ?? 0 ) : null; $d_pos = $cmp ? $pos - round( $cmp['position'] ?? 0, 1 ) : null; $issues = $url_issues[ trailingslashit( $url ) ] ?? array(); $task_html = ''; foreach ( $issues as $issue ) { $sev_c = $issue['severity'] === 'high' ? '#ef4444' : '#f59e0b'; $task_html .= '' . esc_html( mb_substr( $issue['msg'], 0, 22 ) ) . ''; } $dc_html = $d_clicks !== null ? '' . ( $d_clicks > 0 ? '+' : '' ) . $d_clicks . '' : '–'; $dp_html = ( $d_pos !== null && $d_pos != 0 ) ? '' . ( $d_pos > 0 ? '+' : '' ) . round( $d_pos, 1 ) . '' : '–'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html( $col[1] ) . ( ! $no_sort ? ' ↕' : '' ) . '
' . esc_html( strlen( $path ) > 40 ? substr( $path, 0, 37 ) . '…' : $path ) . '' . ( $task_html ? '
' . $task_html . '
' : '✓ OK' ) . '
' . number_format_i18n( $clicks ) . '' . $dc_html . '' . number_format_i18n( $impr ) . '' . $ctr_raw . '%' . $pos . '' . $dp_html . '
'; ?>