PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.2 2.14.1 2.14.0 2.13.0 2.12.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 All 57 releases
← All changes | includes/seo/email-report-sections/class-position-summary-section.php +73 -23 2.0.1 → 2.14.1 View file →
@@ -1,10 +1,12 @@
1 1 <?php
2 2 /**
3 - * Position Summary Section
3 + * Position Summary Section — "Where your keywords rank".
4 4 *
5 - * Buckets tracked keywords into Top 3 / 4–10 / 11–50 / 51–100 brackets.
6 - * Source: GSC `position_distribution` already computed by Analytics_Manager.
5 + * Buckets the period's queries into Top 3 / 4–10 / 11–50 / 51–100 and draws
6 + * them as bars. Source: GSC `position_distribution` already computed by
7 + * Analytics_Manager over the whole property (#913), with queries ranked
8 + * past 100 in `over_100`.
7 9 *
8 10 * @package ThinkRank
9 11 * @subpackage SEO\Email_Report_Sections
10 12 * @since 1.9.0
@@ -24,9 +26,9 @@
24 26 return 'position_summary';
25 27 }
26 28
27 29 public function label(): string {
28 - return __('Position Summary', 'thinkrank');
30 + return __('Where your keywords rank', 'thinkrank');
29 31 }
30 32
31 33 public function default_enabled(): bool {
32 34 return true;
@@ -35,8 +37,12 @@
35 37 public function requires_capability(): ?string {
36 38 return null;
37 39 }
38 40
41 + public function renders_own_heading(): bool {
42 + return true;
43 + }
44 +
39 45 public function collect(array $context): array {
40 46 $shared = $context['shared'] ?? [];
41 47 if (empty($shared['available'])) {
42 48 return [];
@@ -45,36 +51,80 @@
45 51 if (!is_array($dist) || empty($dist)) {
46 52 return [];
47 53 }
48 54 return [
49 - 'top_3' => (int) ($dist['top_3'] ?? 0),
50 - '4_10' => (int) ($dist['4_10'] ?? 0),
51 - '11_50' => (int) ($dist['10_50'] ?? 0), // GSC's `10_50` bucket starts at 11
52 - '51_100' => (int) ($dist['51_100'] ?? 0),
55 + 'top_3' => (int) ($dist['top_3'] ?? 0),
56 + '4_10' => (int) ($dist['4_10'] ?? 0),
57 + '11_50' => (int) ($dist['10_50'] ?? 0), // GSC's `10_50` bucket starts at 11
58 + '51_100' => (int) ($dist['51_100'] ?? 0),
59 + // Counted over the whole property since #913. A payload cached
60 + // before then has neither key and reads as before.
61 + 'over_100' => (int) ($dist['over_100'] ?? 0),
62 + 'complete' => (bool) ($dist['complete'] ?? true),
53 63 ];
54 64 }
55 65
66 + public function has_data(array $payload): bool {
67 + $counts = array_intersect_key($payload, array_flip(['top_3', '4_10', '11_50', '51_100', 'over_100']));
68 + return array_sum(array_map('intval', $counts)) > 0;
69 + }
70 +
56 71 public function render(array $payload): string {
57 - $rows = [
58 - __('Top 3', 'thinkrank') => $payload['top_3'],
59 - __('Position 4–10', 'thinkrank') => $payload['4_10'],
60 - __('Position 11–50', 'thinkrank') => $payload['11_50'],
61 - __('Position 51–100', 'thinkrank') => $payload['51_100'],
72 + $counts = [
73 + 'top_3' => (int) ($payload['top_3'] ?? 0),
74 + '4_10' => (int) ($payload['4_10'] ?? 0),
75 + '11_50' => (int) ($payload['11_50'] ?? 0),
76 + '51_100' => (int) ($payload['51_100'] ?? 0),
62 77 ];
78 + // The bars share out the top 100; queries ranked past it still had
79 + // an impression, so they count toward the headline total (#913).
80 + $ranked = array_sum($counts);
81 + $over_100 = max(0, (int) ($payload['over_100'] ?? 0));
82 + $total = $ranked + $over_100;
83 + if ($total === 0) {
84 + return '';
85 + }
86 + $page_one = $counts['top_3'] + $counts['4_10'];
63 87
64 - $html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">';
65 - foreach ($rows as $label => $count) {
66 - $html .= '<tr>'
67 - . '<td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#374151;font:500 14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">' . esc_html($label) . '</td>'
68 - . '<td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;text-align:right;color:#111827;font:700 14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">' . esc_html(number_format_i18n((int) $count)) . '</td>'
69 - . '</tr>';
88 + // Incomplete means Search Console could not be paged to the end, so
89 + // the count covers the top of the property by clicks, not all of it.
90 + $subtitle = (bool) ($payload['complete'] ?? true)
91 + ? sprintf(
92 + /* translators: %s: number of queries. */
93 + _n('%s query got at least one impression this period', '%s queries got at least one impression this period', $total, 'thinkrank'),
94 + number_format_i18n($total)
95 + )
96 + : sprintf(
97 + /* translators: %s: number of queries counted. */
98 + _n('Counted over your top %s query by clicks this period', 'Counted over your top %s queries by clicks this period', $total, 'thinkrank'),
99 + number_format_i18n($total)
100 + );
101 +
102 + $html = Email_Report_Html::heading($this->label(), $subtitle);
103 + $html .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-top:14px;">'
104 + . Email_Report_Html::bar_row(__('Top 3', 'thinkrank'), $counts['top_3'], $ranked, Email_Report_Html::PRIMARY)
105 + . Email_Report_Html::bar_row(__('Positions 4–10', 'thinkrank'), $counts['4_10'], $ranked, '#6e79ff')
106 + . Email_Report_Html::bar_row(__('Positions 11–50', 'thinkrank'), $counts['11_50'], $ranked, '#b3b9ff')
107 + . Email_Report_Html::bar_row(__('Positions 51–100', 'thinkrank'), $counts['51_100'], $ranked, '#dcdfff')
108 + . '</table>';
109 + if ($over_100 > 0) {
110 + $html .= Email_Report_Html::note(sprintf(
111 + /* translators: 1: queries ranking in the top 100, 2: queries ranking beyond position 100. */
112 + _n('%1$s rank in the top 100 and %2$s ranks beyond position 100.', '%1$s rank in the top 100 and %2$s rank beyond position 100.', $over_100, 'thinkrank'),
113 + number_format_i18n($ranked),
114 + number_format_i18n($over_100)
115 + ));
70 116 }
71 - $html .= '</table>';
117 + $html .= Email_Report_Html::note(sprintf(
118 + /* translators: %s: number of queries ranking in positions 1–10. */
119 + _n('%s query is on page one.', '%s queries are on page one.', $page_one, 'thinkrank'),
120 + number_format_i18n($page_one)
121 + ));
122 + $html .= Email_Report_Html::link(__('See all keywords', 'thinkrank'), Email_Report_Html::admin_link('analytics', 'keywords'));
123 +
72 124 return $html;
73 125 }
74 126
75 127 public function fallback_html(): string {
76 - return '<p style="color:#6b7280;font-style:italic;">'
77 - . esc_html__('Search Console data unavailable. Connect Search Console to see position distribution.', 'thinkrank')
78 - . '</p>';
128 + return '';
79 129 }
80 130 }