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 '

Noch keine ausreichenden Suchdaten im gewählten Zeitraum.

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

🚀 Keywords auf Position 5–15 mit Quick-Win-Potenzial Richtung Top 5 · ' . count( $data ) . ' gefunden

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

Keine Quick-Win-Keywords im aktuellen Zeitraum.

'; return; } $columns = array( array( 'query', 'Suchbegriff', 'left', true ), array( 'page', 'Seite', 'left', true ), array( 'position', 'Position', 'right', true ), array( 'impressions', 'Impressionen', 'right', true ), array( 'clicks', 'Klicks', 'right', true ), array( 'ctr', 'CTR', '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 '

Noch keine ausreichenden Suchdaten im gewählten Zeitraum.

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

📉 Suchbegriffe, deren CTR deutlich unter dem Erwartungswert für ihre Position liegt · ' . count( $data ) . ' gefunden

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

Keine auffälligen CTR-Lecks gefunden.

'; return; } $columns = array( array( 'query', 'Suchbegriff', 'left', true ), array( 'page', 'Seite', 'left', true ), array( 'position', 'Position', 'right', true ), array( 'impressions', 'Impressionen', 'right', true ), array( 'ctr', 'CTR (ist)', 'right', true ), array( 'expected_ctr', 'CTR (erwartet)', '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 '

Noch keine ausreichenden Suchdaten im gewählten Zeitraum.

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

⚔️ Suchbegriffe, für die mehrere eigene Seiten gleichzeitig Impressionen erhalten · ' . count( $data ) . ' betroffene Suchbegriffe · Klick auf eine Zeile zeigt die einzelnen URLs

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

Keine Kannibalisierung erkannt.

'; return; } $path = function( $url ) { return str_replace( home_url(), '', $url ) ?: '/'; }; $table_id = 'aije-gsc-cannib'; echo ''; echo '
'; echo ''; foreach ( array( array( 'query', 'Suchbegriff', 'left' ), array( 'page_count', 'Seiten', 'right' ), array( 'impressions', 'Impressionen (gesamt)', 'right' ), array( 'clicks', 'Klicks (gesamt)', '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 ) ) . 'Details
'; ?> '; echo '
✨

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

'; echo '

Google zeigt seit dem 3. Juni 2026 in der Search Console, wie oft deine Seiten in AI Overviews und AI Mode erscheinen. Diese Daten umfassen aktuell nur Impressionen — keine Klicks oder Suchbegriffe.

'; echo '

Google hat diese Daten bisher nicht 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.

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

Dort: Performance-Bericht öffnen → Filter „Suchtyp" bzw. „Search Appearance" → AI Overviews / AI Mode auswählen.

'; 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 '

Keine Search Console Properties gefunden.

'; 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 '

Keine Daten gefunden.

'; 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', number_format_i18n( $total_clicks ), $fmt( $dc ), '' ), array( 'Impressionen', number_format_i18n( $total_impr ), $fmt( $di ), '' ), array( 'Ø CTR', $avg_ctr . '%', '', '' ), array( 'Ø Position', $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', 'left' ), array( 'tasks', 'Tasks', 'left' ), array( 'clicks', 'Klicks', 'right' ), array( 'clicks_d', '±', 'right' ), array( 'impr', 'Impressionen', 'right' ), array( 'ctr', 'CTR', 'right' ), array( 'pos', 'Ø Position', '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 . '
'; ?>