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 / 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.1.1, at includes/seo/class-author-archives-manager.php

271 lines 9.1 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 // Opens the block through SEO_Manager so its closing comment, printed
111 // on wp_head at priority 99, knows an opener was emitted.
112 \ThinkRank\Frontend\SEO_Manager::note_opening_comment();
113 echo "<!-- ThinkRank SEO Meta Description -->\n";
114 echo '<meta name="description" content="' . esc_attr($description) . '" />' . "\n";
115 echo "<!-- /ThinkRank SEO Meta Description -->\n";
116 }
117
118 /**
119 * Modify document title for author archives
120 *
121 * @since 1.0.0
122 * @param string $title Original title
123 * @return string Modified title
124 */
125 public function modify_document_title(string $title): string {
126 if (is_author()) {
127 $author_id = get_queried_object_id();
128
129 // A per-author SEO title (e.g. imported from another SEO plugin)
130 // overrides the global template entirely.
131 $custom_title = (string) get_user_meta($author_id, '_thinkrank_seo_title', true);
132 if ($custom_title !== '') {
133 return $this->with_page_suffix($custom_title);
134 }
135
136 $settings = Settings::instance();
137 $template = $settings->get('author_archives_title', Settings::DEFAULT_AUTHOR_ARCHIVES_TITLE);
138
139 if (empty($template)) {
140 return $title;
141 }
142
143 // Get variables
144 $author_name = get_the_author_meta('display_name', $author_id);
145 $site_title = get_bloginfo('name');
146
147 // Separator
148 $separator = '-';
149 if (class_exists('ThinkRank\SEO\Site_Identity_Manager')) {
150 $separator = \ThinkRank\SEO\Site_Identity_Manager::get_active_separator_symbol();
151 }
152
153 // %page% resolves to nothing here. This filter runs at priority 15,
154 // after SEO_Manager's at priority 1, so its own page indicator was
155 // the one that reached the page — and it rendered without the
156 // separator the rest of the site uses ("admin | tr Page 2" against
157 // "Uncategorized | tr | Page 2" everywhere else). The token stays
158 // recognised so a template that already contains it does not leak
159 // the literal string; the indicator itself comes from the one
160 // helper every other context uses (#397 review).
161 $replacements = [
162 '%author_name%' => $author_name,
163 '%site_title%' => $site_title,
164 '%separator%' => $separator,
165 '%page%' => ''
166 ];
167
168 $rendered = str_replace(array_keys($replacements), array_values($replacements), $template);
169
170 return $this->with_page_suffix($this->tidy_whitespace($rendered));
171 }
172 return $title;
173 }
174
175 /**
176 * Append the shared page indicator, when SEO_Manager is available.
177 *
178 * @since 2.0.1
179 * @param string $title Rendered author-archive title.
180 * @return string
181 */
182 private function with_page_suffix(string $title): string {
183 if (!class_exists('\ThinkRank\Frontend\SEO_Manager')) {
184 return $title;
185 }
186
187 return \ThinkRank\Frontend\SEO_Manager::with_page_suffix($title);
188 }
189
190 /**
191 * Collapse the gaps left by variables that resolved to nothing.
192 *
193 * %page% is empty on an unpaginated archive, so the stock template ended
194 * every author <title> with a stray trailing space; two empty variables in
195 * a row would leave a double space mid-string.
196 *
197 * @since 1.29.1
198 * @param string $value Rendered template
199 * @return string Template with runs of whitespace collapsed and trimmed
200 */
201 private function tidy_whitespace(string $value): string {
202 return trim((string) preg_replace('/\s+/u', ' ', $value));
203 }
204
205 /**
206 * Filter robots meta tag
207 *
208 * @since 1.0.0
209 * @param array $robots Robots meta array
210 * @return array Filtered robots meta
211 */
212 public function filter_robots(array $robots): array {
213 if (is_author()) {
214 $settings = Settings::instance();
215 $index = $settings->get('author_archives_index', true);
216
217 if (!$index) {
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 } else {
228 // If showing in search results, check if empty archives should be hidden
229 $show_empty = $settings->get('author_archives_show_empty', false);
230 if (!$show_empty) {
231 $author_id = get_queried_object_id();
232 // Check if author has any published posts
233 $post_count = count_user_posts($author_id, 'post', true); // true = only public posts
234 if ((int) $post_count === 0) {
235 // Remove 'index' if present
236 $index_key = array_search('index', $robots, true);
237 if ($index_key !== false) {
238 unset($robots[$index_key]);
239 }
240 // Add 'noindex' if not present
241 if (!in_array('noindex', $robots, true)) {
242 $robots[] = 'noindex';
243 }
244 }
245 }
246 }
247 }
248
249 return array_values($robots);
250 }
251
252 /**
253 * Redirect author archives to homepage if disabled
254 *
255 * @since 1.0.0
256 * @return void
257 */
258 public function maybe_redirect_author_archives(): void {
259 if (is_author()) {
260 $settings = Settings::instance();
261 // Default to true (enabled)
262 $enabled = $settings->get('author_archives_enabled', true);
263
264 if (!$enabled) {
265 wp_safe_redirect(home_url(), 301);
266 exit;
267 }
268 }
269 }
270 }
271