PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.1.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.1.1
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 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / email-report-sections / class-top-posts-renderer.php

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

134 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Top Posts Renderer
4 *
5 * Shared HTML helper used by Top Winning Posts and Top Losing Posts so
6 * both render identically. Not a section itself — purely a stateless
7 * presentation helper.
8 *
9 * @package ThinkRank
10 * @subpackage SEO\Email_Report_Sections
11 * @since 1.9.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\SEO\Email_Report_Sections;
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 final class Top_Posts_Renderer {
23
24 /**
25 * Render a page table (page, clicks, click change vs previous period).
26 *
27 * @param array $rows Each row: ['url'|'page' => string, 'clicks' => int, 'change' => ?int, 'title' => ?string]
28 * @param string $variant 'gain' | 'loss' — controls accent color only.
29 */
30 public static function render(array $rows, string $variant = 'gain'): string {
31 if (empty($rows)) {
32 return '';
33 }
34
35 $accent = $variant === 'loss' ? '#dc2626' : '#16a34a';
36
37 $html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">';
38 $html .= '<tr>'
39 . self::th(__('Page', 'thinkrank'), 'left')
40 . self::th(__('Clicks', 'thinkrank'), 'right')
41 . self::th(__('Change', 'thinkrank'), 'right')
42 . '</tr>';
43
44 foreach ($rows as $row) {
45 $url = (string) ($row['url'] ?? $row['page'] ?? '');
46 $title = (string) ($row['title'] ?? '');
47 $clicks = (int) ($row['clicks'] ?? 0);
48 $display = $title !== '' ? $title : $url;
49
50 $html .= '<tr>'
51 . '<td style="padding:10px;border-bottom:1px solid #f3f4f6;">'
52 . ($url !== ''
53 ? '<a href="' . esc_url($url) . '" style="color:#111827;text-decoration:none;">' . esc_html($display) . '</a>'
54 : '<span style="color:#111827;">' . esc_html($display) . '</span>')
55 . '</td>'
56 . '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">'
57 . esc_html(number_format_i18n($clicks))
58 . '</td>'
59 . self::change_cell($row['change'] ?? null, $accent)
60 . '</tr>';
61 }
62
63 $html .= '</table>';
64 return $html;
65 }
66
67 /**
68 * Render a keyword table (query, position, clicks, click change).
69 *
70 * @param array $rows Each row: ['query' => string, 'position' => ?float, 'clicks' => int, 'change' => ?int]
71 * @param string $variant 'gain' | 'loss'
72 */
73 public static function render_keywords(array $rows, string $variant = 'gain'): string {
74 if (empty($rows)) {
75 return '';
76 }
77
78 $accent = $variant === 'loss' ? '#dc2626' : '#16a34a';
79
80 $html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">';
81 $html .= '<tr>'
82 . self::th(__('Keyword', 'thinkrank'), 'left')
83 . self::th(__('Position', 'thinkrank'), 'right')
84 . self::th(__('Clicks', 'thinkrank'), 'right')
85 . self::th(__('Change', 'thinkrank'), 'right')
86 . '</tr>';
87
88 foreach ($rows as $row) {
89 $query = (string) ($row['query'] ?? ($row['keys'][0] ?? ''));
90 // Null position means "no data this period" (e.g. a keyword that
91 // dropped out entirely) — a literal 0.0 would read as rank #1.
92 $position = isset($row['position'])
93 ? number_format_i18n((float) $row['position'], 1)
94 : '';
95 $clicks = (int) ($row['clicks'] ?? 0);
96 if ($query === '') {
97 continue;
98 }
99 $html .= '<tr>'
100 . '<td style="padding:10px;border-bottom:1px solid #f3f4f6;color:#111827;">' . esc_html($query) . '</td>'
101 . '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">' . esc_html($position) . '</td>'
102 . '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;">' . esc_html(number_format_i18n($clicks)) . '</td>'
103 . self::change_cell($row['change'] ?? null, $accent)
104 . '</tr>';
105 }
106
107 $html .= '</table>';
108 return $html;
109 }
110
111 /**
112 * Table header cell.
113 */
114 private static function th(string $label, string $align): string {
115 return '<th style="text-align:' . esc_attr($align) . ';padding:8px 10px;border-bottom:1px solid #e5e7eb;font:600 12px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#6b7280;text-transform:uppercase;letter-spacing:.04em;">'
116 . esc_html($label) . '</th>';
117 }
118
119 /**
120 * Signed click-change cell. Positive renders green with ▲, negative red
121 * with ▼. A null/zero change renders a neutral dash.
122 */
123 private static function change_cell($change, string $accent): string {
124 $td = '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:%s;">%s</td>';
125
126 if ($change === null || (int) $change === 0) {
127 return sprintf($td, '#9ca3af', '');
128 }
129 $change = (int) $change;
130 $arrow = $change > 0 ? '' : '';
131 return sprintf($td, esc_attr($accent), esc_html($arrow . ' ' . number_format_i18n(abs($change))));
132 }
133 }
134