PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.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 1.27.0 1.26.0 1.25.0 trunk All 53 releases
← All changes | includes/seo/class-seo-analyzer-fixer.php +150 -12 2.4.0 → 2.12.0 View file →
@@ -72,8 +72,19 @@
72 72 'image_alt_text' => [
73 73 'label' => __('Fill in missing image alt text', 'thinkrank'),
74 74 'warning' => __('Runs in batches over your media library. With AI alt text enabled this uses your AI provider key.', 'thinkrank'),
75 75 ],
76 + 'ai_crawler_access' => [
77 + 'label' => __('Allow AI answer engines to crawl this site', 'thinkrank'),
78 + // Only the answer engines are unblocked. Saying so matters:
79 + // people block the training crawlers on purpose, and a fix
80 + // that quietly reversed that decision would be a betrayal.
81 + 'warning' => __('Sets the answer-engine crawlers (ChatGPT, Claude, Perplexity, Google-Extended) to Allow. Crawlers that only collect training data keep whatever setting you gave them.', 'thinkrank'),
82 + ],
83 + 'llms_txt' => [
84 + 'label' => __('Publish llms.txt', 'thinkrank'),
85 + 'warning' => __('Publishes /llms.txt from your saved LLMs.txt settings. It needs those fields filled in first.', 'thinkrank'),
86 + ],
76 87 ];
77 88
78 89 /**
79 90 * Filter the checks the analyzer can fix automatically.
@@ -118,8 +129,12 @@
118 129 case 'schema':
119 130 return $this->fix_schema();
120 131 case 'image_alt_text':
121 132 return $this->fix_image_alt_text();
133 + case 'ai_crawler_access':
134 + return $this->fix_ai_crawler_access();
135 + case 'llms_txt':
136 + return $this->fix_llms_txt();
122 137 }
123 138
124 139 /**
125 140 * Let a third party handle a fix it registered.
@@ -285,25 +300,148 @@
285 300 ],
286 301 ];
287 302 }
288 303
304 + $message = $remaining > 0
305 + ? sprintf(
306 + /* translators: 1: images updated in this batch, 2: images still to process. */
307 + __('Added alt text to %1$d images. %2$d still to go — run the fix again to continue.', 'thinkrank'),
308 + $updated,
309 + $remaining
310 + )
311 + : sprintf(
312 + /* translators: %d: number of images updated. */
313 + __('Added alt text to %d images. Your media library is done.', 'thinkrank'),
314 + $updated
315 + );
316 +
317 + // The write is done, but a cache ThinkRank cannot reach still decides
318 + // what the visitor sees. Saying so beats a green toast over a page
319 + // that has not changed (#763).
320 + $warnings = (array) ($result['cache']['warnings'] ?? []);
321 + if ([] !== $warnings) {
322 + $message .= ' ' . implode(' ', $warnings);
323 + }
324 +
289 325 return [
290 326 'fixed' => true,
291 - 'message' => $remaining > 0
292 - ? sprintf(
293 - /* translators: 1: images updated in this batch, 2: images still to process. */
294 - __('Added alt text to %1$d images. %2$d still to go — run the fix again to continue.', 'thinkrank'),
295 - $updated,
296 - $remaining
297 - )
298 - : sprintf(
299 - /* translators: %d: number of images updated. */
300 - __('Added alt text to %d images. Your media library is done.', 'thinkrank'),
301 - $updated
302 - ),
327 + 'message' => $message,
303 328 'data' => [
304 329 'updated' => $updated,
305 330 'remaining' => $remaining,
306 331 ],
332 + ];
333 + }
334 +
335 + /**
336 + * Set every AI answer-engine crawler to `allow`.
337 + *
338 + * Deliberately narrow: only the crawlers that decide whether the site can
339 + * be CITED are touched. GPTBot, ClaudeBot, CCBot and the other
340 + * training-only agents keep whatever the user chose for them — blocking
341 + * those is an editorial position, not a misconfiguration.
342 + *
343 + * @since 2.5.0
344 + * @return array
345 + * @throws \Exception When the rules cannot be saved, or when something
346 + * other than the rule map is still blocking them.
347 + */
348 + private function fix_ai_crawler_access(): array {
349 + $manager = new Site_Identity_Manager();
350 + $settings = $manager->get_settings('site');
351 + $settings = is_array($settings) ? $settings : [];
352 +
353 + $rules = isset($settings['ai_crawler_rules']) && is_array($settings['ai_crawler_rules'])
354 + ? $settings['ai_crawler_rules']
355 + : [];
356 +
357 + foreach (SEO_Analyzer::GEO_ANSWER_AGENTS as $slug) {
358 + if (AI_Crawlers::exists($slug)) {
359 + $rules[$slug] = 'allow';
360 + }
361 + }
362 +
363 + $settings['ai_crawler_rules'] = $rules;
364 +
365 + if (!$manager->save_settings('site', null, $settings)) {
366 + throw new \Exception(esc_html__('The AI crawler rules could not be saved. Check the PHP error log for the ThinkRank line naming the cause.', 'thinkrank'));
367 + }
368 +
369 + // The directives are composed into the served body at render time, but
370 + // a physical robots.txt in the web root is a copy the web server hands
371 + // out directly — saving the rules does not touch it. Re-sync from a
372 + // fresh manager (the one above holds the pre-save settings) or the fix
373 + // changes only the file nobody is reading.
374 + (new Site_Identity_Manager())->sync_robots_txt_file();
375 +
376 + // The rules are only one of the things that can disallow a crawler: a
377 + // hand-written robots.txt body, a physical robots.txt in the web root,
378 + // and the site-wide search block all outrank them. Ask the served file
379 + // again rather than reporting a success it contradicts.
380 + $still_blocked = (new SEO_Analyzer())->blocked_answer_agents();
381 +
382 + if (!empty($still_blocked)) {
383 + throw new \Exception(
384 + sprintf(
385 + /* translators: %s: comma-separated crawler names. */
386 + esc_html__('The crawler rules were saved, but your robots.txt still blocks %s. That block comes from your own robots.txt content, a robots.txt file in your site root, or the site-wide search-engine setting — edit it under Essential SEO → Crawling and AI Indexing.', 'thinkrank'),
387 + esc_html(implode(', ', $still_blocked))
388 + )
389 + );
390 + }
391 +
392 + return [
393 + 'fixed' => true,
394 + 'message' => __('AI answer engines can now crawl and cite your site.', 'thinkrank'),
395 + 'data' => [],
396 + ];
397 + }
398 +
399 + /**
400 + * Generate and publish /llms.txt from the saved LLMs.txt settings.
401 + *
402 + * The document is built from settings the user already wrote, so this is a
403 + * publish, not authorship. Incomplete settings produce an invalid document
404 + * and are refused rather than written — a half-empty llms.txt is worse
405 + * than none, because an assistant will read it and believe it.
406 + *
407 + * @since 2.5.0
408 + * @return array
409 + * @throws \Exception When the module is unavailable, the settings are
410 + * incomplete, or the write fails.
411 + */
412 + private function fix_llms_txt(): array {
413 + if (!class_exists('ThinkRank\\SEO\\LLMs_Txt_Manager')) {
414 + throw new \Exception(esc_html__('The llms.txt module is unavailable.', 'thinkrank'));
415 + }
416 +
417 + $manager = new LLMs_Txt_Manager();
418 + $generated = $manager->generate_llms_txt([], []);
419 +
420 + $validation = isset($generated['validation']) && is_array($generated['validation'])
421 + ? $generated['validation']
422 + : [];
423 +
424 + if (empty($validation['valid'])) {
425 + throw new \Exception(esc_html__('llms.txt could not be published because its settings are incomplete. Fill in Website Description, Key Features and Target Audience under Essential SEO → Crawling and AI Indexing → LLMs.txt, then try again.', 'thinkrank'));
426 + }
427 +
428 + $content = (string) ($generated['content'] ?? '');
429 + $write = $manager->write_llms_txt_to_file($content);
430 +
431 + if (empty($write['success'])) {
432 + $reason = (string) ($write['message'] ?? '');
433 +
434 + throw new \Exception(
435 + '' !== $reason
436 + ? esc_html($reason)
437 + : esc_html__('llms.txt could not be written. Check that your site root is writable.', 'thinkrank')
438 + );
439 + }
440 +
441 + return [
442 + 'fixed' => true,
443 + 'message' => __('Your llms.txt is now published at /llms.txt.', 'thinkrank'),
444 + 'data' => [],
307 445 ];
308 446 }
309 447 }