PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.6.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.6.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 All 52 releases
thinkrank / includes / seo / email-report-sections / class-top-losing-keywords-section.php

class-top-losing-keywords-section.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.6.0, at includes/seo/email-report-sections/class-top-losing-keywords-section.php

86 lines 2.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Top Losing Keywords Section
4 *
5 * Queries with the largest click loss vs the previous period, computed from
6 * the shared current-vs-previous comparison the Data Provider builds from
7 * Search Console (query dimension). Ranking is on the click delta, not on
8 * average position — position is carried alongside as context only, and is
9 * null for a query that dropped out of the current window entirely.
10 *
11 * @package ThinkRank
12 * @subpackage SEO\Email_Report_Sections
13 * @since 1.9.0
14 */
15
16 declare(strict_types=1);
17
18 namespace ThinkRank\SEO\Email_Report_Sections;
19
20 if (!defined('ABSPATH')) {
21 exit;
22 }
23
24 final class Top_Losing_Keywords_Section implements Email_Report_Section_Interface {
25
26 private const MAX_ROWS = 5;
27
28 public function key(): string {
29 return 'top_losing_keywords';
30 }
31
32 public function label(): string {
33 return __('Top Losing Keywords', 'thinkrank');
34 }
35
36 public function default_enabled(): bool {
37 return true;
38 }
39
40 public function requires_capability(): ?string {
41 return null;
42 }
43
44 public function collect(array $context): array {
45 $shared = $context['shared'] ?? [];
46 $comparison = $shared['comparison'] ?? [];
47 if (empty($comparison['available']) || empty($comparison['queries'])) {
48 return [];
49 }
50
51 // Real losers: keywords whose clicks fell vs the previous period.
52 $rows = [];
53 foreach ($comparison['queries'] as $entry) {
54 $delta = (int) $entry['cur_clicks'] - (int) $entry['prev_clicks'];
55 if ($delta >= 0) {
56 continue;
57 }
58 $rows[] = [
59 'query' => $entry['query'] ?? '',
60 // A keyword that dropped out entirely has no current position.
61 // Keep it null so the renderer shows a dash instead of "0.0".
62 'position' => isset($entry['cur_pos']) ? (float) $entry['cur_pos'] : null,
63 'clicks' => (int) $entry['cur_clicks'],
64 'change' => $delta,
65 ];
66 }
67
68 // Biggest click loss first (most negative).
69 usort($rows, static fn($a, $b) => $a['change'] <=> $b['change']);
70
71 return [
72 'rows' => array_slice($rows, 0, self::MAX_ROWS),
73 ];
74 }
75
76 public function render(array $payload): string {
77 return Top_Posts_Renderer::render_keywords($payload['rows'] ?? [], 'loss');
78 }
79
80 public function fallback_html(): string {
81 return '<p style="color:#6b7280;font-style:italic;">'
82 . esc_html__('Search Console data unavailable. Connect Search Console to see top losing keywords.', 'thinkrank')
83 . '</p>';
84 }
85 }
86