ID, '_guest_author', true); wp_nonce_field('wp_multi_guest_author_nonce', 'wp_multi_guest_author_nonce_field'); ?> ID, '_guest_author', true); if (!empty($guest_author)) { $author_name = $guest_author; } } } return $author_name; } add_filter('the_author', 'wp_multi_display_guest_author'); // Anzeige des Gast-Autors in der Beitragsübersicht (Backend) function wp_multi_add_guest_author_column($columns) { if (isset($columns['author'])) { $columns['guest_author'] = __('Gast-Autor', 'wp-multi'); } return $columns; } add_filter('manage_posts_columns', 'wp_multi_add_guest_author_column'); function wp_multi_display_guest_author_column($column_name, $post_id) { if ($column_name == 'guest_author') { $guest_author = get_post_meta($post_id, '_guest_author', true); echo !empty($guest_author) ? esc_html($guest_author) : __('Kein Gast-Autor', 'wp-multi'); } } add_action('manage_posts_custom_column', 'wp_multi_display_guest_author_column', 10, 2); // Der Gast-Autoren-Menüpunkt wird zentral in wp_multi_register_admin_menus() registriert. /** * Ermittelt den Aktivitätsstatus eines Gast-Autors anhand seiner letzten Geschichte. * * 🟢 Aktiv – letzte Geschichte innerhalb der letzten 60 Tage * 🟡 Inaktiv – letzte Geschichte 61–180 Tage her * 🔴 Stark inaktiv – letzte Geschichte länger als 180 Tage her * ⭐ Ehemaliger Vielschreiber – mindestens 10 Geschichten, aber seit über 180 Tagen inaktiv * * Der Rang steigt mit dem "Rückhol-Potenzial": absteigend sortiert stehen die * ehemaligen Vielschreiber ganz oben, die aktiven Autoren ganz unten. * * @param string $last_post_date Datum der letzten Geschichte (MySQL-Format, Seitenzeit). * @param int $post_count Anzahl veröffentlichter Geschichten. * @return array|null Array mit key, emoji, label, class, rank und days – oder null ohne Datum. */ function wp_multi_get_guest_author_activity($last_post_date, $post_count = 0) { if (empty($last_post_date)) { return null; } // Über GMT rechnen, damit die Zeitzone der Seite das Ergebnis nicht verschiebt $last_ts = strtotime(get_gmt_from_date($last_post_date)); if (!$last_ts) { return null; } $days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS); if ($days <= 60) { $status = ['key' => 'active', 'emoji' => '🟢', 'label' => __('Aktiv', 'wp-multi'), 'class' => 'wpm-badge--success', 'rank' => 1]; } elseif ($days <= 180) { $status = ['key' => 'inactive', 'emoji' => '🟡', 'label' => __('Inaktiv', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2]; } elseif ((int) $post_count >= 10) { $status = ['key' => 'former_prolific', 'emoji' => '⭐', 'label' => __('Ehemaliger Vielschreiber', 'wp-multi'), 'class' => 'wpm-badge--star', 'rank' => 4]; } else { $status = ['key' => 'dormant', 'emoji' => '🔴', 'label' => __('Stark inaktiv', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 3]; } $status['days'] = $days; return $status; } /** * Liefert die Beschriftungen aller Aktivitätsstatus für Filter und Legende. */ function wp_multi_get_guest_author_status_labels() { return [ 'former_prolific' => '⭐ ' . __('Ehemaliger Vielschreiber', 'wp-multi'), 'dormant' => '🔴 ' . __('Stark inaktiv', 'wp-multi'), 'inactive' => '🟡 ' . __('Inaktiv', 'wp-multi'), 'active' => '🟢 ' . __('Aktiv', 'wp-multi'), ]; } /** * Liefert die Rangleiter der Autoren, absteigend nach Schwelle. * * 🎉 Newcomer ab 5 – ⭐ Vielschreiber ab 10 – 🏆 Etablierter Autor ab 25 – * 👑 Star-Autor ab 50 – 💎 Legende ab 100 Geschichten. * * @return array Zuordnung "Schwelle => [emoji, Bezeichnung]". */ function wp_multi_get_guest_author_ranks() { return [ 100 => ['💎', __('Legende', 'wp-multi')], 50 => ['👑', __('Star-Autor', 'wp-multi')], 25 => ['🏆', __('Etablierter Autor', 'wp-multi')], 10 => ['⭐', __('Vielschreiber', 'wp-multi')], 5 => ['🎉', __('Newcomer', 'wp-multi')], ]; } /** * Ermittelt den höchsten erreichten Rang eines Autors. * * Es wird immer nur der höchste erreichte Rang zurückgegeben, nie mehrere. * * @param int $post_count Anzahl veröffentlichter Geschichten. * @return array|null Array mit threshold, emoji, label und label_full – oder null ohne Rang. */ function wp_multi_get_guest_author_milestone($post_count) { $post_count = (int) $post_count; foreach (wp_multi_get_guest_author_ranks() as $threshold => $rank) { if ($post_count >= $threshold) { return [ 'threshold' => $threshold, 'emoji' => $rank[0], 'label' => $rank[1], /* translators: 1: Rangbezeichnung, 2: Schwelle des Rangs */ 'label_full' => sprintf(__('%1$s (ab %2$d Geschichten)', 'wp-multi'), $rank[1], $threshold), ]; } } return null; } /** * Ermittelt den nächsten noch nicht erreichten Rang. * * @param int $post_count Anzahl veröffentlichter Geschichten. * @return array|null Array mit threshold, emoji, label und missing – oder null nach dem letzten Rang. */ function wp_multi_get_guest_author_next_milestone($post_count) { $post_count = (int) $post_count; // Aufsteigend, damit der nächste offene Rang zuerst greift $ranks = array_reverse(wp_multi_get_guest_author_ranks(), true); foreach ($ranks as $threshold => $rank) { if ($post_count < $threshold) { return [ 'threshold' => $threshold, 'emoji' => $rank[0], 'label' => $rank[1], 'missing' => $threshold - $post_count, ]; } } return null; } /** * Zählt die veröffentlichten Beiträge eines Gast-Autors. * * Nutzt dieselbe Regel wie die Autoren-Übersicht, damit Übersicht und * Autorenprofil nie unterschiedliche Zahlen anzeigen. * * @param string $guest_author Name des Gast-Autors. * @return int */ function wp_multi_get_guest_author_post_count($guest_author) { if (empty($guest_author)) { return 0; } global $wpdb; return (int) $wpdb->get_var($wpdb->prepare(" SELECT COUNT(*) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = '_guest_author' AND pm.meta_value = %s AND p.post_status = 'publish' ", $guest_author)); } /** * Liefert die Aufrufe je Gast-Autor aus der Analytics-Tabelle. * * Gezählt werden alle Besucher (siehe wp_multi_post_view_activity()); Bots sind * ausgenommen und ein Besucher zählt je Beitrag höchstens einmal pro halbe Stunde. * * @return array|null Zuordnung "Autorenname => Aufrufe" oder null ohne Analytics-Tabelle. */ function wp_multi_get_guest_author_views() { if (!defined('WP_MULTI_ANALYTICS_TABLE')) { return null; } global $wpdb; $table = WP_MULTI_ANALYTICS_TABLE; if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) { return null; } $results = $wpdb->get_results(" SELECT pm.meta_value AS guest_author, COUNT(*) AS views FROM {$table} a INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author' WHERE a.action = 'view' AND pm.meta_value != '' GROUP BY pm.meta_value "); $views = []; foreach ((array) $results as $row) { $views[$row->guest_author] = (int) $row->views; } return $views; } /** * Liefert die Reiter der Gast-Autoren-Seite. */ function wp_multi_get_guest_author_tabs() { return [ 'authors' => __('Gast-Autoren', 'wp-multi'), 'series' => __('Liegengebliebene Serien', 'wp-multi'), 'views' => __('Aufrufe je Geschichte', 'wp-multi'), ]; } // Callback-Funktion für die Gast-Autor-Übersicht function wp_multi_guest_author_overview_page() { $tabs = wp_multi_get_guest_author_tabs(); $current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'authors'; if (!isset($tabs[$current_tab])) { $current_tab = 'authors'; } $base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php')); wpmt_admin_page_open( __('Gast-Autoren', 'wp-multi'), __('Alle Gast-Autoren mit Anzahl ihrer veröffentlichten Beiträge und ihrem Aktivitätsstatus', 'wp-multi') ); ?> ' . esc_html__('Die Serien-Erkennung konnte nicht ausgeführt werden.', 'wp-multi') . '

'; wpmt_admin_card_close(); } } elseif ($current_tab === 'views') { wp_multi_render_post_views_table($base_url); } else { wp_multi_render_guest_author_table($base_url); } wpmt_admin_page_close(); } /** * Rendert die Aufrufe einzelner Geschichten mit dem Datum des letzten Aufrufs. * * @param string $base_url Basis-URL der Übersichtsseite. */ function wp_multi_render_post_views_table($base_url) { wpmt_admin_card_open(__('Aufrufe je Geschichte', 'wp-multi')); if (!defined('WP_MULTI_ANALYTICS_TABLE')) { echo '

' . esc_html__('Das Analytics-Modul ist nicht aktiv, deshalb liegen keine Aufrufe vor.', 'wp-multi') . '

'; wpmt_admin_card_close(); return; } global $wpdb; $table = WP_MULTI_ANALYTICS_TABLE; if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) { echo '

' . esc_html__('Die Analytics-Tabelle existiert noch nicht. Sie wird bei der Aktivierung des Plugins angelegt.', 'wp-multi') . '

'; wpmt_admin_card_close(); return; } // Sortierparameter aus URL lesen (Whitelist gegen SQL-Injection) $valid_orderby = ['views' => 'views', 'last_view' => 'last_view', 'title' => 'p.post_title']; // Standard: zuletzt gelesene Geschichten oben $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'last_view'; if (!isset($valid_orderby[$orderby])) { $orderby = 'last_view'; } $order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC'; $sql_orderby = $valid_orderby[$orderby]; // Lange Listen bremsen die Seite aus – Anzahl per Filter anpassbar $limit = (int) apply_filters('wp_multi_post_views_limit', 100); $sort_url = add_query_arg('tab', 'views', $base_url); $posts = $wpdb->get_results($wpdb->prepare(" SELECT a.post_id, p.post_title, p.post_status, COUNT(*) AS views, MAX(a.timestamp) AS last_view, pm.meta_value AS guest_author FROM {$table} a INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' WHERE a.action = 'view' GROUP BY a.post_id, p.post_title, p.post_status, pm.meta_value ORDER BY {$sql_orderby} {$order}, p.post_title ASC LIMIT %d ", $limit)); $total_views = 0; foreach ((array) $posts as $row) { $total_views += (int) $row->views; } $date_format = get_option('date_format') . ' ' . get_option('time_format'); ?>

post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?> post_status !== 'publish') : ?> (post_status); ?>) guest_author, $base_url, 'views'); ?> views)); ?> last_view))); ?> (last_view)))); ?>)
= $limit) : ?>

['series_count' => int, 'max_parts' => int]" oder null ohne Serien. */ function wp_multi_get_guest_author_series_stats() { $source = wp_multi_detect_series_source(); if (!$source) { return null; } global $wpdb; $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : ''; $results = $wpdb->get_results($wpdb->prepare(" SELECT x.guest_author, SUM(CASE WHEN x.parts >= 2 THEN 1 ELSE 0 END) AS series_count, MAX(x.parts) AS max_parts FROM ( SELECT pm.meta_value AS guest_author, tt.term_id, COUNT(DISTINCT p.ID) AS parts FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != '' INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql} WHERE p.post_status = 'publish' GROUP BY pm.meta_value, tt.term_id ) x GROUP BY x.guest_author ", $source['taxonomy'])); $stats = []; foreach ((array) $results as $row) { $stats[$row->guest_author] = [ 'series_count' => (int) $row->series_count, 'max_parts' => (int) $row->max_parts, ]; } return $stats; } /** * Durchschnittliche Aufrufe je Geschichte über alle Gast-Autoren hinweg. * * Maßstab für "gute Aufrufe" in der Reaktivierungsbewertung. Wird einmal je * Seitenaufruf berechnet, damit Übersicht und Detailansicht identisch bewerten. * * @return float */ function wp_multi_get_average_guest_author_views() { static $average = null; if ($average !== null) { return $average; } $table = wp_multi_get_analytics_table(); if (!$table) { $average = 0.0; return $average; } global $wpdb; $views = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$table} a INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author' AND pm.meta_value != '' WHERE a.action = 'view' "); $posts = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != '' WHERE p.post_status = 'publish' "); $average = $posts ? ($views / $posts) : 0.0; return $average; } /** * Bewertet, wie lohnend es ist, einen Autor zurückzuholen. * * 🔥 Sehr interessant – ab 10 Geschichten, mindestens ein halbes Jahr still, * mit einer eigenen Serie von mindestens 3 Teilen * ⭐ Interessant – ab 5 Geschichten, mindestens ein halbes Jahr still, * und überdurchschnittlich gelesen * 🟡 Beobachten – ab 3 Geschichten und mindestens ein halbes Jahr still * ⚪ Keine Priorität – 1–2 Geschichten, lange still * – Kein Bedarf – schreibt noch * * @param array $data Kennzahlen des Autors (post_count, days, views, series_count, max_parts). * @param float $avg_views Durchschnittliche Aufrufe je Geschichte auf der ganzen Seite. * @return array Array mit key, emoji, label, class und rank. */ function wp_multi_get_guest_author_reactivation($data, $avg_views = 0) { $post_count = isset($data['post_count']) ? (int) $data['post_count'] : 0; $days = isset($data['days']) ? (int) $data['days'] : 0; $views = isset($data['views']) ? (int) $data['views'] : 0; $series_count = isset($data['series_count']) ? (int) $data['series_count'] : 0; $max_parts = isset($data['max_parts']) ? (int) $data['max_parts'] : 0; $min_days = (int) apply_filters('wp_multi_reactivation_min_days', 180); // Wer noch schreibt, muss nicht zurückgeholt werden if ($days < $min_days) { return ['key' => 'current', 'emoji' => '–', 'label' => __('Kein Bedarf', 'wp-multi'), 'class' => '', 'rank' => 0]; } if ($post_count >= 10 && $series_count > 0 && $max_parts >= 3) { return ['key' => 'hot', 'emoji' => '🔥', 'label' => __('Sehr interessant', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 4]; } // "Mehrere Geschichten mit guten Aufrufen": im Schnitt deutlich über dem // Seitendurchschnitt gelesen. Ohne Analytics-Daten greift die Regel nicht. $good_views_factor = (float) apply_filters('wp_multi_reactivation_views_factor', 1.5); $has_good_views = ($avg_views > 0 && $post_count > 0 && ($views / $post_count) >= ($avg_views * $good_views_factor)); if ($post_count >= 5 && $has_good_views) { return ['key' => 'interesting', 'emoji' => '⭐', 'label' => __('Interessant', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 3]; } if ($post_count >= 3) { return ['key' => 'watch', 'emoji' => '🟡', 'label' => __('Beobachten', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2]; } return ['key' => 'low', 'emoji' => '⚪', 'label' => __('Keine Priorität', 'wp-multi'), 'class' => '', 'rank' => 1]; } /** * Liefert die Beschriftungen aller Reaktivierungsstufen für den Filter. */ function wp_multi_get_reactivation_labels() { return [ 'hot' => '🔥 ' . __('Sehr interessant', 'wp-multi'), 'interesting' => '⭐ ' . __('Interessant', 'wp-multi'), 'watch' => '🟡 ' . __('Beobachten', 'wp-multi'), 'low' => '⚪ ' . __('Keine Priorität', 'wp-multi'), 'current' => '– ' . __('Kein Bedarf', 'wp-multi'), ]; } /** * Gibt eine Filterzeile mit Zählern aus. * * @param string $title Beschriftung der Zeile. * @param string $param Name des URL-Parameters. * @param array $labels Zuordnung "Wert => Beschriftung". * @param array $counts Zuordnung "Wert => Anzahl". * @param string $active Aktuell aktiver Wert ('' = alle). * @param int $total Gesamtzahl für "Alle". * @param string $link_url Basis-URL für die Links. */ function wp_multi_render_filter_row($title, $param, $labels, $counts, $active, $total, $link_url) { ?>

$label) : ?> | ()

'pm.meta_value', 'posts' => 'post_count', 'date' => 'last_post_date']; $php_orderby = ['status', 'views', 'series', 'potential']; if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) { $orderby = 'guest_author'; } $sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'pm.meta_value'; $sql_order = $order; // URLs für Sortier- und Filter-Links (Reiter, Filter und Sortierung bleiben erhalten) $base_url = add_query_arg('tab', 'authors', $base_url); $sort_url = $base_url; if ($filter) { $sort_url = add_query_arg('activity', $filter, $sort_url); } if ($potential) { $sort_url = add_query_arg('potential', $potential, $sort_url); } $filter_url = add_query_arg(['orderby' => $orderby, 'order' => $order], $base_url); $status_url = $potential ? add_query_arg('potential', $potential, $filter_url) : $filter_url; $potential_url = $filter ? add_query_arg('activity', $filter, $filter_url) : $filter_url; global $wpdb; $query = " SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = '_guest_author' AND pm.meta_value != '' AND p.post_status = 'publish' GROUP BY pm.meta_value ORDER BY {$sql_orderby} {$sql_order}, pm.meta_value ASC "; $guest_authors = $wpdb->get_results($query); // Durchschnittliche Aufrufe je Geschichte – Maßstab für "gute Aufrufe" $avg_views = wp_multi_get_average_guest_author_views(); // Kennzahlen ergänzen, Autoren je Filterwert zählen und einschränken $status_counts = array_fill_keys(array_keys($status_labels), 0); $reactivation_counts = array_fill_keys(array_keys($reactivation_labels), 0); $rows = []; $position = 0; foreach ((array) $guest_authors as $author) { $author->activity = wp_multi_get_guest_author_activity($author->last_post_date, $author->post_count); $author->views = $show_views && isset($views[$author->guest_author]) ? $views[$author->guest_author] : 0; $author->series_count = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['series_count'] : 0; $author->max_parts = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['max_parts'] : 0; $author->position = $position++; // Reihenfolge aus der DB (korrekte Sortierung inkl. Umlauten) $author->reactivation = wp_multi_get_guest_author_reactivation([ 'post_count' => (int) $author->post_count, 'days' => $author->activity ? $author->activity['days'] : 0, 'views' => $author->views, 'series_count' => $author->series_count, 'max_parts' => $author->max_parts, ], $avg_views); if ($author->activity) { $status_counts[$author->activity['key']]++; } $reactivation_counts[$author->reactivation['key']]++; if ($filter !== '' && (!$author->activity || $author->activity['key'] !== $filter)) { continue; } if ($potential !== '' && $author->reactivation['key'] !== $potential) { continue; } $rows[] = $author; } // Sortierung nach berechneten Spalten: bei Gleichstand die DB-Reihenfolge behalten if (in_array($orderby, $php_orderby, true)) { usort($rows, function ($a, $b) use ($orderby, $order) { switch ($orderby) { case 'views': $value_a = (int) $a->views; $value_b = (int) $b->views; break; case 'series': $value_a = (int) $a->series_count; $value_b = (int) $b->series_count; break; case 'potential': $value_a = (int) $a->reactivation['rank']; $value_b = (int) $b->reactivation['rank']; break; default: $value_a = $a->activity ? $a->activity['rank'] : 0; $value_b = $b->activity ? $b->activity['rank'] : 0; } if ($value_a === $value_b) { return $a->position - $b->position; } $diff = $value_a - $value_b; return ($order === 'DESC') ? -$diff : $diff; }); } $colspan = 4 + ($show_views ? 1 : 0) + ($show_series ? 1 : 0); wpmt_admin_card_open(); wp_multi_render_filter_row( __('Status:', 'wp-multi'), 'activity', $status_labels, $status_counts, $filter, count((array) $guest_authors), $status_url ); wp_multi_render_filter_row( __('Reaktivierung:', 'wp-multi'), 'potential', $reactivation_labels, $reactivation_counts, $potential, count((array) $guest_authors), $potential_url ); ?>
activity; ?>
guest_author, $base_url, 'authors'); ?> post_count)); ?> series_count) : ?> series_count)); ?> views)); ?> last_post_date)); ?> (last_post_date))))); ?>) reactivation; ?>

$tab, 'guest_author' => $name], $base_url); } /** * Gibt einen oder mehrere Autorennamen als Links zur Detailansicht aus. * * @param string $names Ein Name oder mehrere, mit ", " verbunden. * @param string $base_url Basis-URL der Übersichtsseite. * @param string $tab Aktueller Reiter. * @param bool $split Bei true wird an ", " getrennt (GROUP_CONCAT-Listen). */ function wp_multi_render_guest_author_link($names, $base_url, $tab = 'authors', $split = false) { if ($names === null || $names === '') { echo ''; return; } $list = $split ? array_filter(array_map('trim', explode(',', $names))) : [$names]; $links = []; foreach ($list as $name) { $links[] = '' . esc_html($name) . ''; } echo implode(', ', $links); } /** * Zeigt alles zu einem Gast-Autor auf einer Seite: Kennzahlen, Serien und * Einzelgeschichten. Grundlage für die Entscheidung, wen man kontaktiert. * * @param string $base_url Basis-URL der Übersichtsseite. * @param string $guest_author Name des Gast-Autors. * @param string $tab Reiter, aus dem der Aufruf kam. */ function wp_multi_render_guest_author_detail($base_url, $guest_author, $tab = 'authors') { global $wpdb; $back_url = add_query_arg('tab', $tab, $base_url); // Kennzahlen des Autors $summary = $wpdb->get_row($wpdb->prepare(" SELECT COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID WHERE pm.meta_key = '_guest_author' AND pm.meta_value = %s AND p.post_status = 'publish' ", $guest_author)); // Der Name stammt aus einem Meta-Feld und wird deshalb selbst escaped // ausgegeben, statt ihn als Kartentitel durchzureichen. wpmt_admin_card_open(__('Autoren-Details', 'wp-multi')); ?>

post_count) { echo '

' . esc_html__('Zu diesem Namen wurden keine veröffentlichten Geschichten gefunden.', 'wp-multi') . '

'; wpmt_admin_card_close(); return; } $post_count = (int) $summary->post_count; $activity = wp_multi_get_guest_author_activity($summary->last_post_date, $post_count); $milestone = wp_multi_get_guest_author_milestone($post_count); // Gesamtaufrufe aller Geschichten dieses Autors $analytics_table = wp_multi_get_analytics_table(); $total_views = null; if ($analytics_table) { $total_views = (int) $wpdb->get_var($wpdb->prepare(" SELECT COUNT(*) FROM {$analytics_table} a INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author' WHERE a.action = 'view' AND pm.meta_value = %s ", $guest_author)); } ?> prepare('AND tt.parent = %d', $source['parent']) : ''; $series = $wpdb->get_results($wpdb->prepare(" SELECT t.term_id, t.name AS series_name, COUNT(DISTINCT p.ID) AS author_parts, MAX(p.post_date) AS last_part_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql} INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id WHERE p.post_status = 'publish' GROUP BY t.term_id, t.name ORDER BY last_part_date DESC ", $guest_author, $source['taxonomy'])); if ($series) { // Gesamtteile der Serie (auch von anderen Autoren) und Aufrufe nachladen $term_ids = []; foreach ($series as $row) { $term_ids[] = (int) $row->term_id; } $term_id_list = implode(',', $term_ids); $totals = $wpdb->get_results($wpdb->prepare(" SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE p.post_status = 'publish' AND tt.term_id IN ({$term_id_list}) GROUP BY tt.term_id ", $source['taxonomy'])); $total_parts = []; foreach ((array) $totals as $total) { $total_parts[(int) $total->term_id] = (int) $total->total_parts; } wp_multi_add_series_details($series, $source['taxonomy']); // Beiträge in Serien merken, damit sie unten nicht doppelt auftauchen $ids = $wpdb->get_col($wpdb->prepare(" SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE tt.term_id IN ({$term_id_list}) ", $source['taxonomy'])); foreach ((array) $ids as $id) { $series_post_ids[] = (int) $id; } ?>

term_id, $source['taxonomy']); $parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts; ?>
series_name); ?> series_name); ?> author_parts !== (int) $parts) : ?> author_parts); ?> last_part_id) : ?> last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
last_part_date)); ?> (last_part_date))))); ?>)
series_views)); ?>
get_results($wpdb->prepare(" SELECT p.ID, p.post_title, p.post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' WHERE pm.meta_value = %s AND p.post_status = 'publish' {$exclude_sql} ORDER BY p.post_date DESC LIMIT %d ", $guest_author, $limit)); // Aufrufe der angezeigten Geschichten $post_views = []; if ($analytics_table && $posts) { $post_ids = []; foreach ($posts as $row) { $post_ids[] = (int) $row->ID; } $post_id_list = implode(',', $post_ids); $view_rows = $wpdb->get_results(" SELECT post_id, COUNT(*) AS views FROM {$analytics_table} WHERE action = 'view' AND post_id IN ({$post_id_list}) GROUP BY post_id "); foreach ((array) $view_rows as $view_row) { $post_views[(int) $view_row->post_id] = (int) $view_row->views; } } ?>

post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?> post_date)); ?> ID]) ? $post_views[(int) $row->ID] : 0)); ?>
string, 'parent' => int, 'label' => string] oder null. */ function wp_multi_detect_series_source() { // Feste Vorgabe per Filter, z. B. add_filter('wp_multi_series_taxonomy', fn() => 'meine-serien'); $forced = apply_filters('wp_multi_series_taxonomy', ''); if ($forced && taxonomy_exists($forced)) { return ['taxonomy' => $forced, 'parent' => 0, 'label' => $forced]; } // Mögliche Namen einer eigenen Serien-Taxonomie $candidates = apply_filters('wp_multi_series_taxonomy_candidates', ['series', 'serie', 'serien', 'story-series']); foreach ($candidates as $candidate) { if (!taxonomy_exists($candidate)) { continue; } $count = wp_count_terms(['taxonomy' => $candidate, 'hide_empty' => false]); if (!is_wp_error($count) && (int) $count > 0) { return ['taxonomy' => $candidate, 'parent' => 0, 'label' => $candidate]; } } // Zweites Modell: Oberkategorie mit einer Unterkategorie je Serie foreach ($candidates as $candidate) { $term = get_term_by('slug', $candidate, 'category'); if (!$term || is_wp_error($term)) { continue; } $children = get_term_children($term->term_id, 'category'); if (!is_wp_error($children) && !empty($children)) { return [ 'taxonomy' => 'category', 'parent' => (int) $term->term_id, /* translators: %s: Name der Oberkategorie */ 'label' => sprintf(__('Unterkategorien von "%s"', 'wp-multi'), $term->name), ]; } } return null; } /** * Gibt einen Spaltenkopf mit Sortier-Link aus. * * @param string $label Beschriftung der Spalte. * @param string $key Sortierschlüssel dieser Spalte. * @param string $orderby Aktuell aktive Sortierung. * @param string $order Aktuelle Richtung (ASC/DESC). * @param string $sort_url Basis-URL für den Link. * @param string $default_order Richtung beim ersten Klick auf diese Spalte. */ function wp_multi_render_sort_link($label, $key, $orderby, $order, $sort_url, $default_order = 'ASC') { $is_active = ($orderby === $key); // Aktive Spalte kehrt die Richtung um, inaktive startet mit ihrem Standard $next_order = $is_active ? (($order === 'ASC') ? 'DESC' : 'ASC') : $default_order; $url = add_query_arg(['orderby' => $key, 'order' => $next_order], $sort_url); echo '' . esc_html($label); if ($is_active) { echo ' ' . (($order === 'ASC') ? '↑' : '↓') . ''; } echo ''; } /** * Liefert den Namen der Analytics-Tabelle, sofern sie existiert. * * @return string Tabellenname oder '' ohne Analytics-Modul. */ function wp_multi_get_analytics_table() { static $table = null; if ($table !== null) { return $table; } if (!defined('WP_MULTI_ANALYTICS_TABLE')) { $table = ''; return $table; } global $wpdb; $name = WP_MULTI_ANALYTICS_TABLE; $table = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $name)) ? $name : ''; return $table; } /** * Ergänzt Serien um letzten Teil und Aufrufzahlen. * * Läuft in eigenen Abfragen statt als weiterer JOIN in der Hauptabfrage: Ein * zusätzlicher JOIN würde die Zeilen vervielfachen und die Teile-Zählung * verfälschen. * * @param array $rows Serienzeilen mit term_id. * @param string $taxonomy Taxonomie der Serien. */ function wp_multi_add_series_details(&$rows, $taxonomy) { if (empty($rows)) { return; } global $wpdb; $term_ids = []; foreach ($rows as $row) { $term_ids[] = (int) $row->term_id; } $term_id_list = implode(',', $term_ids); // Letzter Teil je Serie: absteigend sortiert, der erste Treffer je Serie gewinnt $parts = $wpdb->get_results($wpdb->prepare(" SELECT tt.term_id, p.ID AS post_id, p.post_title FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE p.post_status = 'publish' AND tt.term_id IN ({$term_id_list}) ORDER BY p.post_date DESC ", $taxonomy)); $last_parts = []; foreach ((array) $parts as $part) { if (!isset($last_parts[$part->term_id])) { $last_parts[$part->term_id] = $part; } } // Aufrufe der ganzen Serie und des letzten Teils $series_views = []; $post_views = []; $analytics_table = wp_multi_get_analytics_table(); if ($analytics_table) { $totals = $wpdb->get_results($wpdb->prepare(" SELECT tt.term_id, COUNT(*) AS views FROM {$analytics_table} a INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = a.post_id INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE a.action = 'view' AND tt.term_id IN ({$term_id_list}) GROUP BY tt.term_id ", $taxonomy)); foreach ((array) $totals as $total) { $series_views[(int) $total->term_id] = (int) $total->views; } $post_ids = []; foreach ($last_parts as $part) { $post_ids[] = (int) $part->post_id; } if ($post_ids) { $post_id_list = implode(',', $post_ids); $single = $wpdb->get_results(" SELECT post_id, COUNT(*) AS views FROM {$analytics_table} WHERE action = 'view' AND post_id IN ({$post_id_list}) GROUP BY post_id "); foreach ((array) $single as $entry) { $post_views[(int) $entry->post_id] = (int) $entry->views; } } } foreach ($rows as $row) { $term_id = (int) $row->term_id; $last_part = isset($last_parts[$term_id]) ? $last_parts[$term_id] : null; $row->last_part_id = $last_part ? (int) $last_part->post_id : 0; $row->last_part_title = $last_part ? $last_part->post_title : ''; $row->series_views = isset($series_views[$term_id]) ? $series_views[$term_id] : 0; $row->last_part_views = ($row->last_part_id && isset($post_views[$row->last_part_id])) ? $post_views[$row->last_part_id] : 0; } } /** * Zeigt Serien, die seit längerem keinen neuen Teil bekommen haben. * * Beispiel: "Serie mit 7 Teilen – letzter Teil vor 11 Monaten". Damit sieht man * im Backend sofort, wo sich Nachfragen beim Autor lohnt. */ function wp_multi_render_stalled_series($base_url = '') { // Mindestens so viele Teile und so viele Tage ohne Fortsetzung $min_parts = (int) apply_filters('wp_multi_stalled_series_min_parts', 2); $min_days = (int) apply_filters('wp_multi_stalled_series_min_days', 180); if ($base_url === '') { $base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php')); } $sort_url = add_query_arg('tab', 'series', $base_url); // Sortierparameter aus URL lesen. 'series_views' und 'last_part_views' stammen // aus den Nachfragen und werden deshalb in PHP sortiert. $valid_orderby = ['name' => 't.name', 'parts' => 'part_count', 'date' => 'last_part_date']; $php_orderby = ['series_views', 'last_part_views']; $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'date'; if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) { $orderby = 'date'; } // Standard: neueste Fortsetzung oben $order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC'; $sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'last_part_date'; wpmt_admin_card_open(__('Liegengebliebene Serien', 'wp-multi')); $source = wp_multi_detect_series_source(); if (!$source) { wp_multi_render_series_source_hint(); wpmt_admin_card_close(); return; } global $wpdb; // Bei Unterkategorien nur die Kinder der Oberkategorie berücksichtigen $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : ''; $series = $wpdb->get_results($wpdb->prepare(" SELECT t.term_id, t.name AS series_name, COUNT(DISTINCT p.ID) AS part_count, MAX(p.post_date) AS last_part_date, GROUP_CONCAT(DISTINCT pm.meta_value ORDER BY pm.meta_value SEPARATOR ', ') AS authors FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql} INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != '' WHERE p.post_status = 'publish' GROUP BY t.term_id, t.name HAVING part_count >= %d ORDER BY {$sql_orderby} {$order}, last_part_date DESC ", $source['taxonomy'], $min_parts)); // Nur Serien behalten, deren letzter Teil lange genug zurückliegt $stalled = []; foreach ((array) $series as $row) { $last_ts = strtotime(get_gmt_from_date($row->last_part_date)); if (!$last_ts) { continue; } $row->days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS); if ($row->days > $min_days) { $stalled[] = $row; } } // Letzten Teil und Aufrufe nur für die tatsächlich angezeigten Serien nachladen wp_multi_add_series_details($stalled, $source['taxonomy']); // Sortierung nach Aufrufen: bei Gleichstand die DB-Reihenfolge behalten if (in_array($orderby, $php_orderby, true)) { $position = 0; foreach ($stalled as $row) { $row->position = $position++; } usort($stalled, function ($a, $b) use ($orderby, $order) { $value_a = (int) $a->$orderby; $value_b = (int) $b->$orderby; if ($value_a === $value_b) { return $a->position - $b->position; } $diff = $value_a - $value_b; return ($order === 'DESC') ? -$diff : $diff; }); } $show_views = (bool) wp_multi_get_analytics_table(); $colspan = $show_views ? 6 : 4; ?>

term_id, $source['taxonomy']); ?>
series_name); ?> series_name); ?> part_count; ?> last_part_id) : ?> last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
last_part_date)); ?> (last_part_date))))); ?>)
series_views)); ?> last_part_views)); ?> authors, $base_url, 'series', true); ?>

true], 'objects'); ?>

$taxonomy->name, 'hide_empty' => false]); ?>
name); ?> labels->name); ?>

add_filter('wp_multi_series_taxonomy', function () { return 'taxonomie-name'; });

$page_title, 'post_name' => $page_slug, 'post_content' => '[guest_author_posts]', 'post_status' => 'publish', 'post_type' => 'page', )); if ($page_id && !is_wp_error($page_id)) { update_option('wp_multi_guest_author_page_id', $page_id); } } else { update_option('wp_multi_guest_author_page_id', $page->ID); } } add_action('init', 'wp_multi_create_guest_author_page'); /** * Liefert die Serien eines Gast-Autors. * * @param string $guest_author Name des Gast-Autors. * @return array ['series' => Zeilen, 'post_ids' => IDs aller Teile dieser Serien] */ function wp_multi_get_guest_author_series($guest_author) { $empty = ['series' => [], 'post_ids' => []]; $source = wp_multi_detect_series_source(); if (!$source || $guest_author === '') { return $empty; } global $wpdb; $parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : ''; $series = $wpdb->get_results($wpdb->prepare(" SELECT t.term_id, t.name AS series_name, COUNT(DISTINCT p.ID) AS author_parts, MAX(p.post_date) AS last_part_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql} INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id WHERE p.post_status = 'publish' GROUP BY t.term_id, t.name ORDER BY last_part_date DESC ", $guest_author, $source['taxonomy'])); if (!$series) { return $empty; } $term_ids = []; foreach ($series as $row) { $term_ids[] = (int) $row->term_id; } $term_id_list = implode(',', $term_ids); // Gesamtlänge der Serie – auch Teile anderer Autoren zählen dazu $totals = $wpdb->get_results($wpdb->prepare(" SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE p.post_status = 'publish' AND tt.term_id IN ({$term_id_list}) GROUP BY tt.term_id ", $source['taxonomy'])); $total_parts = []; foreach ((array) $totals as $total) { $total_parts[(int) $total->term_id] = (int) $total->total_parts; } foreach ($series as $row) { $row->total_parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts; $row->taxonomy = $source['taxonomy']; } // Alle Beiträge dieser Serien – damit sie in der Einzelliste nicht doppelt stehen $post_ids = $wpdb->get_col($wpdb->prepare(" SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s WHERE tt.term_id IN ({$term_id_list}) ", $source['taxonomy'])); return [ 'series' => $series, 'post_ids' => array_map('intval', (array) $post_ids), ]; } /** * Findet die Seite, auf der die Autorenübersicht steht. * * Gesucht wird die Seite mit dem Shortcode [guest_authors]; das Ergebnis wird * zwischengespeichert, damit die Suche nicht bei jedem Profilaufruf läuft. * * @return string URL oder '' wenn keine Übersichtsseite existiert. */ function wp_multi_get_guest_author_overview_url() { $url = apply_filters('wp_multi_author_overview_url', ''); if ($url) { return $url; } $cached = get_transient('wp_multi_author_overview_page'); if ($cached === false) { global $wpdb; $cached = (int) $wpdb->get_var(" SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE '%[guest_authors%' ORDER BY ID ASC LIMIT 1 "); set_transient('wp_multi_author_overview_page', $cached, 12 * HOUR_IN_SECONDS); } return $cached ? get_permalink($cached) : ''; } // Shortcode für Gastautoren-Liste function wp_multi_guest_author_shortcode($atts = []) { ob_start(); global $wpdb; // Erlaubte Sortierfelder (Whitelist – verhindert SQL-Injection) $valid_orderby = [ 'name' => 'pm.meta_value', 'posts' => 'post_count', 'date' => 'last_post_date', 'new' => 'first_post_date', ]; // Sinnvolle Standardrichtung je Spalte $default_order = ['name' => 'ASC', 'posts' => 'DESC', 'date' => 'DESC', 'new' => 'DESC']; // Voreinstellung über Shortcode-Attribute, z. B. [guest_authors orderby="date" order="desc"] $atts = shortcode_atts(['orderby' => 'name', 'order' => ''], $atts, 'guest_authors'); // URL-Parameter haben Vorrang vor den Shortcode-Attributen $orderby = isset($_GET['ga_orderby']) ? sanitize_key(wp_unslash($_GET['ga_orderby'])) : sanitize_key($atts['orderby']); if (!isset($valid_orderby[$orderby])) { $orderby = 'name'; } $order_raw = isset($_GET['ga_order']) ? sanitize_key(wp_unslash($_GET['ga_order'])) : sanitize_key($atts['order']); $order = in_array(strtoupper($order_raw), ['ASC', 'DESC'], true) ? strtoupper($order_raw) : $default_order[$orderby]; $sql_orderby = $valid_orderby[$orderby]; // Alle Gastautoren mit Beitragsanzahl, erster und letzter Veröffentlichung $query = " SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date, MIN(p.post_date) AS first_post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = '_guest_author' AND pm.meta_value != '' AND p.post_status = 'publish' GROUP BY pm.meta_value ORDER BY {$sql_orderby} {$order}, pm.meta_value ASC "; $guest_authors = $wpdb->get_results($query); // Serien je Autor (leer, wenn es keine Serien-Taxonomie gibt) $series_stats = wp_multi_get_guest_author_series_stats(); // URL der Gastautoren-Beitragsseite $guest_author_page_id = get_option('wp_multi_guest_author_page_id'); $guest_author_page_url = $guest_author_page_id ? get_permalink($guest_author_page_id) : '#'; // Beschriftungen der Sortier-Schaltflächen $sort_labels = [ 'name' => __('Name', 'wp-multi'), 'posts' => __('Meiste Geschichten', 'wp-multi'), 'date' => __('Zuletzt veröffentlicht', 'wp-multi'), 'new' => __('Neueste Autoren', 'wp-multi'), ]; ?>
$label) : ?> $key, 'ga_order' => strtolower($next_order)]) . '#guest-author-section'; ?>
post_count); $series_count = (is_array($series_stats) && isset($series_stats[$author->guest_author])) ? $series_stats[$author->guest_author]['series_count'] : 0; ?>

guest_author); ?>

post_count, 'wp-multi')), esc_html(number_format_i18n($author->post_count))); ?> ·

last_post_date)) : ?>

last_post_date))); ?>

get_results($wpdb->prepare(" SELECT p.ID, p.post_title, p.post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' WHERE pm.meta_value = %s AND p.post_status = 'publish' {$exclude_sql} ORDER BY p.post_date DESC LIMIT %d ", $guest_author, $latest_limit)); // Beliebteste Geschichten – nur wenn Aufrufe vorliegen $popular = []; $analytics_table = wp_multi_get_analytics_table(); if ($analytics_table) { $popular_limit = (int) apply_filters('wp_multi_author_profile_popular_limit', 5); $popular = $wpdb->get_results($wpdb->prepare(" SELECT p.ID, p.post_title, COUNT(*) AS views FROM {$analytics_table} a INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' WHERE a.action = 'view' AND pm.meta_value = %s AND p.post_status = 'publish' GROUP BY p.ID, p.post_title ORDER BY views DESC LIMIT %d ", $guest_author, $popular_limit)); } ?>

·

term_id, $row->taxonomy); ?>

series_name); ?> series_name); ?>

total_parts, 'wp-multi')), esc_html(number_format_i18n($row->total_parts))); ?> · last_part_date))); ?>

ID)); ?>

post_date)); ?>