PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.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 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / class-author-archives-manager.php

class-author-archives-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.0, at includes/seo/class-author-archives-manager.php

254 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Author Archives Manager
4 *
5 * Handles author archives functionality including redirects
6 *
7 * @package ThinkRank
8 * @subpackage SEO
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\SEO;
15
16 // Prevent direct access.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 use ThinkRank\Core\Settings;
22
23 /**
24 * Author Archives Manager Class
25 *
26 * @since 1.0.0
27 */
28 class Author_Archives_Manager {
29
30 /**
31 * Initialize the manager
32 *
33 * @since 1.0.0
34 * @return void
35 */
36 public function init(): void {
37 add_action('template_redirect', [$this, 'maybe_redirect_author_archives']);
38 add_filter('thinkrank_robots_meta', [$this, 'filter_robots']);
39 add_filter('pre_get_document_title', [$this, 'modify_document_title'], 15);
40 add_action('wp_head', [$this, 'add_meta_description'], 5);
41 }
42
43 /**
44 * Add meta description for author archives
45 *
46 * This is the only place a description is printed for author archives —
47 * SEO_Manager::output_meta_description() returns early on is_author() so
48 * the two never both emit a <meta name="description"> tag.
49 *
50 * @since 1.0.0
51 * @return void
52 */
53 public function add_meta_description(): void {
54 if (!is_author()) {
55 return;
56 }
57
58 $author_id = get_queried_object_id();
59
60 // A per-author meta description (e.g. imported from another SEO plugin)
61 // overrides the global template.
62 $custom_desc = (string) get_user_meta($author_id, '_thinkrank_meta_description', true);
63 if ($custom_desc !== '') {
64 $this->print_meta_description($custom_desc);
65 return;
66 }
67
68 $settings = Settings::instance();
69 // Default value matches what we set in endpoint
70 $template = $settings->get('author_archives_meta_desc', Settings::DEFAULT_AUTHOR_ARCHIVES_META_DESC);
71 if (empty($template)) {
72 return;
73 }
74
75 // Get variables
76 $author_name = get_the_author_meta('display_name', $author_id);
77 $site_title = get_bloginfo('name');
78
79 $replacements = [
80 '%author_name%' => $author_name,
81 '%site_title%' => $site_title
82 ];
83
84 $meta_desc = str_replace(array_keys($replacements), array_values($replacements), $template);
85
86 $this->print_meta_description($meta_desc);
87 }
88
89 /**
90 * Print the description tag with the same framing SEO_Manager uses.
91 *
92 * Keeps author archives consistent with every other page type: the plugin
93 * header comment, the 160-character trim, and the open/close markers.
94 *
95 * @since 1.29.1
96 * @param string $description Resolved description text
97 * @return void
98 */
99 private function print_meta_description(string $description): void {
100 $description = $this->tidy_whitespace($description);
101 if ($description === '') {
102 return;
103 }
104
105 // Ensure description is within optimal length (150-160 characters)
106 if (strlen($description) > 160) {
107 $description = wp_trim_words($description, 25, '...');
108 }
109
110 echo "<!-- Search Engine Optimization by ThinkRank - https://thinkrank.ai/ -->\n";
111 echo "<!-- ThinkRank SEO Meta Description -->\n";
112 echo '<meta name="description" content="' . esc_attr($description) . '" />' . "\n";
113 echo "<!-- /ThinkRank SEO Meta Description -->\n";
114 }
115
116 /**
117 * Modify document title for author archives
118 *
119 * @since 1.0.0
120 * @param string $title Original title
121 * @return string Modified title
122 */
123 public function modify_document_title(string $title): string {
124 if (is_author()) {
125 $author_id = get_queried_object_id();
126
127 // A per-author SEO title (e.g. imported from another SEO plugin)
128 // overrides the global template entirely.
129 $custom_title = (string) get_user_meta($author_id, '_thinkrank_seo_title', true);
130 if ($custom_title !== '') {
131 return $custom_title;
132 }
133
134 $settings = Settings::instance();
135 $template = $settings->get('author_archives_title', Settings::DEFAULT_AUTHOR_ARCHIVES_TITLE);
136
137 if (empty($template)) {
138 return $title;
139 }
140
141 // Get variables
142 $author_name = get_the_author_meta('display_name', $author_id);
143 $site_title = get_bloginfo('name');
144
145 // Separator
146 $separator = '-';
147 if (class_exists('ThinkRank\SEO\Site_Identity_Manager')) {
148 $separator = \ThinkRank\SEO\Site_Identity_Manager::get_active_separator_symbol();
149 }
150
151 // Page number
152 $page_str = '';
153 $paged = get_query_var('paged') ? (int) get_query_var('paged') : 1;
154 if ($paged > 1) {
155 // translators: %d is the page number for paginated author archives.
156 $page_str = sprintf(__('Page %d', 'thinkrank'), $paged);
157 }
158
159 $replacements = [
160 '%author_name%' => $author_name,
161 '%site_title%' => $site_title,
162 '%separator%' => $separator,
163 '%page%' => $page_str
164 ];
165
166 $rendered = str_replace(array_keys($replacements), array_values($replacements), $template);
167
168 return $this->tidy_whitespace($rendered);
169 }
170 return $title;
171 }
172
173 /**
174 * Collapse the gaps left by variables that resolved to nothing.
175 *
176 * %page% is empty on an unpaginated archive, so the stock template ended
177 * every author <title> with a stray trailing space; two empty variables in
178 * a row would leave a double space mid-string.
179 *
180 * @since 1.29.1
181 * @param string $value Rendered template
182 * @return string Template with runs of whitespace collapsed and trimmed
183 */
184 private function tidy_whitespace(string $value): string {
185 return trim((string) preg_replace('/\s+/u', ' ', $value));
186 }
187
188 /**
189 * Filter robots meta tag
190 *
191 * @since 1.0.0
192 * @param array $robots Robots meta array
193 * @return array Filtered robots meta
194 */
195 public function filter_robots(array $robots): array {
196 if (is_author()) {
197 $settings = Settings::instance();
198 $index = $settings->get('author_archives_index', true);
199
200 if (!$index) {
201 // Remove 'index' if present
202 $index_key = array_search('index', $robots, true);
203 if ($index_key !== false) {
204 unset($robots[$index_key]);
205 }
206 // Add 'noindex' if not present
207 if (!in_array('noindex', $robots, true)) {
208 $robots[] = 'noindex';
209 }
210 } else {
211 // If showing in search results, check if empty archives should be hidden
212 $show_empty = $settings->get('author_archives_show_empty', false);
213 if (!$show_empty) {
214 $author_id = get_queried_object_id();
215 // Check if author has any published posts
216 $post_count = count_user_posts($author_id, 'post', true); // true = only public posts
217 if ((int) $post_count === 0) {
218 // Remove 'index' if present
219 $index_key = array_search('index', $robots, true);
220 if ($index_key !== false) {
221 unset($robots[$index_key]);
222 }
223 // Add 'noindex' if not present
224 if (!in_array('noindex', $robots, true)) {
225 $robots[] = 'noindex';
226 }
227 }
228 }
229 }
230 }
231
232 return array_values($robots);
233 }
234
235 /**
236 * Redirect author archives to homepage if disabled
237 *
238 * @since 1.0.0
239 * @return void
240 */
241 public function maybe_redirect_author_archives(): void {
242 if (is_author()) {
243 $settings = Settings::instance();
244 // Default to true (enabled)
245 $enabled = $settings->get('author_archives_enabled', true);
246
247 if (!$enabled) {
248 wp_safe_redirect(home_url(), 301);
249 exit;
250 }
251 }
252 }
253 }
254