| @@ -101,14 +101,19 @@ | ||
| 101 | 101 | if ($description === '') { |
| 102 | 102 | return; |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | - // Ensure description is within optimal length (150-160 characters) | |
| 106 | - if (strlen($description) > 160) { | |
| 107 | - $description = wp_trim_words($description, 25, '...'); | |
| 108 | - } | |
| 105 | + // Keep the rendered description within the ~160 characters search | |
| 106 | + // engines display. Measure and cut in CHARACTERS: strlen() counts | |
| 107 | + // BYTES, so a Cyrillic/CJK description tripped a limit it was nowhere | |
| 108 | + // near, and wp_trim_words() cuts by WORD COUNT, so a long-worded | |
| 109 | + // description sailed past the cap entirely. Three units, three | |
| 110 | + // different answers. | |
| 111 | + $description = $this->trim_to_length($description, self::DESCRIPTION_MAX_LENGTH); | |
| 109 | 112 | |
| 110 | - echo "<!-- Search Engine Optimization by ThinkRank - https://thinkrank.ai/ -->\n"; | |
| 113 | + // Opens the block through SEO_Manager so its closing comment, printed | |
| 114 | + // on wp_head at priority 99, knows an opener was emitted. | |
| 115 | + \ThinkRank\Frontend\SEO_Manager::note_opening_comment(); | |
| 111 | 116 | echo "<!-- ThinkRank SEO Meta Description -->\n"; |
| 112 | 117 | echo '<meta name="description" content="' . esc_attr($description) . '" />' . "\n"; |
| 113 | 118 | echo "<!-- /ThinkRank SEO Meta Description -->\n"; |
| 114 | 119 | } |
| @@ -113,8 +118,36 @@ | ||
| 113 | 118 | echo "<!-- /ThinkRank SEO Meta Description -->\n"; |
| 114 | 119 | } |
| 115 | 120 | |
| 116 | 121 | /** |
| 122 | + * Characters search engines display for a meta description. | |
| 123 | + * | |
| 124 | + * @since 2.2.0 | |
| 125 | + * @var int | |
| 126 | + */ | |
| 127 | + private const DESCRIPTION_MAX_LENGTH = 160; | |
| 128 | + | |
| 129 | + /** | |
| 130 | + * Trim a description to a character budget, multibyte-safe. | |
| 131 | + * | |
| 132 | + * Cuts on a word boundary when one is available inside the budget, so the | |
| 133 | + * result does not end mid-word; falls back to a hard character cut for | |
| 134 | + * scripts that do not use spaces (CJK), where a word-boundary search would | |
| 135 | + * find nothing and return the string untouched. | |
| 136 | + * | |
| 137 | + * @since 2.2.0 | |
| 138 | + * @param string $description Description text. | |
| 139 | + * @param int $limit Maximum length in characters, ellipsis included. | |
| 140 | + * @return string | |
| 141 | + */ | |
| 142 | + private function trim_to_length(string $description, int $limit): string { | |
| 143 | + // The implementation moved to Seo_Text so the four other | |
| 144 | + // description paths could stop carrying the broken version of it | |
| 145 | + // (#687). This stays as the local name the author-archive code reads. | |
| 146 | + return \ThinkRank\Core\Seo_Text::trim_to_length($description, $limit); | |
| 147 | + } | |
| 148 | + | |
| 149 | + /** | |
| 117 | 150 | * Modify document title for author archives |
| 118 | 151 | * |
| 119 | 152 | * @since 1.0.0 |
| 120 | 153 | * @param string $title Original title |
| @@ -127,9 +160,9 @@ | ||
| 127 | 160 | // A per-author SEO title (e.g. imported from another SEO plugin) |
| 128 | 161 | // overrides the global template entirely. |
| 129 | 162 | $custom_title = (string) get_user_meta($author_id, '_thinkrank_seo_title', true); |
| 130 | 163 | if ($custom_title !== '') { |
| 131 | - return $custom_title; | |
| 164 | + return $this->with_page_suffix($custom_title); | |
| 132 | 165 | } |
| 133 | 166 | |
| 134 | 167 | $settings = Settings::instance(); |
| 135 | 168 | $template = $settings->get('author_archives_title', Settings::DEFAULT_AUTHOR_ARCHIVES_TITLE); |
| @@ -147,31 +180,46 @@ | ||
| 147 | 180 | if (class_exists('ThinkRank\SEO\Site_Identity_Manager')) { |
| 148 | 181 | $separator = \ThinkRank\SEO\Site_Identity_Manager::get_active_separator_symbol(); |
| 149 | 182 | } |
| 150 | 183 | |
| 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 | - | |
| 184 | + // %page% resolves to nothing here. This filter runs at priority 15, | |
| 185 | + // after SEO_Manager's at priority 1, so its own page indicator was | |
| 186 | + // the one that reached the page — and it rendered without the | |
| 187 | + // separator the rest of the site uses ("admin | tr Page 2" against | |
| 188 | + // "Uncategorized | tr | Page 2" everywhere else). The token stays | |
| 189 | + // recognised so a template that already contains it does not leak | |
| 190 | + // the literal string; the indicator itself comes from the one | |
| 191 | + // helper every other context uses (#397 review). | |
| 159 | 192 | $replacements = [ |
| 160 | 193 | '%author_name%' => $author_name, |
| 161 | 194 | '%site_title%' => $site_title, |
| 162 | 195 | '%separator%' => $separator, |
| 163 | - '%page%' => $page_str | |
| 196 | + '%page%' => '' | |
| 164 | 197 | ]; |
| 165 | 198 | |
| 166 | 199 | $rendered = str_replace(array_keys($replacements), array_values($replacements), $template); |
| 167 | 200 | |
| 168 | - return $this->tidy_whitespace($rendered); | |
| 201 | + return $this->with_page_suffix($this->tidy_whitespace($rendered)); | |
| 169 | 202 | } |
| 170 | 203 | return $title; |
| 171 | 204 | } |
| 172 | 205 | |
| 173 | 206 | /** |
| 207 | + * Append the shared page indicator, when SEO_Manager is available. | |
| 208 | + * | |
| 209 | + * @since 2.0.1 | |
| 210 | + * @param string $title Rendered author-archive title. | |
| 211 | + * @return string | |
| 212 | + */ | |
| 213 | + private function with_page_suffix(string $title): string { | |
| 214 | + if (!class_exists('\ThinkRank\Frontend\SEO_Manager')) { | |
| 215 | + return $title; | |
| 216 | + } | |
| 217 | + | |
| 218 | + return \ThinkRank\Frontend\SEO_Manager::with_page_suffix($title); | |
| 219 | + } | |
| 220 | + | |
| 221 | + /** | |
| 174 | 222 | * Collapse the gaps left by variables that resolved to nothing. |
| 175 | 223 | * |
| 176 | 224 | * %page% is empty on an unpaginated archive, so the stock template ended |
| 177 | 225 | * every author <title> with a stray trailing space; two empty variables in |
| @@ -244,9 +292,14 @@ | ||
| 244 | 292 | // Default to true (enabled) |
| 245 | 293 | $enabled = $settings->get('author_archives_enabled', true); |
| 246 | 294 | |
| 247 | 295 | if (!$enabled) { |
| 248 | - wp_safe_redirect(home_url(), 301); | |
| 296 | + // 302, not 301. This redirect lasts exactly as long as the | |
| 297 | + // setting stays off, but a 301 is cached by browsers and CDNs | |
| 298 | + // indefinitely — so turning author archives back on could not | |
| 299 | + // undo it for anyone who had already been redirected, and there | |
| 300 | + // was no server-side way to fix that. | |
| 301 | + wp_safe_redirect(home_url(), 302); | |
| 249 | 302 | exit; |
| 250 | 303 | } |
| 251 | 304 | } |
| 252 | 305 | } |