PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.2 2.14.1 2.14.0 2.13.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 All 57 releases
← All changes | includes/seo/class-sitemap-generator.php +115 -47 2.12.0 → 2.14.1 View file →
@@ -409,11 +409,105 @@
409 409 add_action($hook, static function (int $term_id, int $tt_id, string $taxonomy): void {
410 410 self::listener()->handle_taxonomy_change($term_id, $tt_id, $taxonomy);
411 411 }, 20, 3);
412 412 }
413 +
414 + // The sitemap leaves out what the robots tag noindexes and what
415 + // ThinkRank redirects (#911), so a change to either decides what the
416 + // published files should list. Neither is a post or term save, and
417 + // without these the files kept the old answer until unrelated content
418 + // changed.
419 + foreach (self::ROBOTS_SETTINGS_OPTIONS as $option) {
420 + add_action('update_option_' . $option, static function ($old_value, $value): void {
421 + self::handle_robots_settings_change($old_value, $value);
422 + }, 20, 2);
423 + add_action('add_option_' . $option, static function ($name, $value): void {
424 + self::handle_robots_settings_change([], $value);
425 + }, 20, 2);
426 + }
427 +
428 + add_action('thinkrank_object_redirect_saved', static function (): void {
429 + self::listener()->schedule_regeneration();
430 + }, 20, 0);
413 431 }
414 432
415 433 /**
434 + * Options holding robots directives the sitemap's item filter reads.
435 + *
436 + * @since 2.15.0
437 + * @var string[]
438 + */
439 + private const ROBOTS_SETTINGS_OPTIONS = [
440 + 'thinkrank_global_seo_settings',
441 + 'thinkrank_global_robot_meta_settings',
442 + ];
443 +
444 + /**
445 + * Queue a rebuild when a saved robots setting changes a noindex decision.
446 + *
447 + * Both options carry far more than robots directives (titles, schema,
448 + * feature switches), so only a change to a noindex outcome rebuilds.
449 + *
450 + * @since 2.15.0
451 + *
452 + * @param mixed $old_value Previous option value.
453 + * @param mixed $value New option value.
454 + * @return void
455 + */
456 + public static function handle_robots_settings_change($old_value, $value): void {
457 + if (self::noindex_fingerprint($old_value) === self::noindex_fingerprint($value)) {
458 + return;
459 + }
460 +
461 + // The matrix memoises the option for the request, and a rebuild that
462 + // runs in this request must read the value just saved.
463 + Content_Type_Settings::flush_cache();
464 +
465 + self::listener()->schedule_regeneration();
466 + }
467 +
468 + /**
469 + * The noindex decisions a robots option makes, keyed by what they apply to.
470 + *
471 + * Reads both shapes: the flat site-wide directives, and the per-entity
472 + * rows, where a row's directives apply only while its robots switch is on.
473 + * A row that is on but stores no `noindex` key changes nothing, matching
474 + * the array_merge() the robots tag does.
475 + *
476 + * @since 2.15.0
477 + *
478 + * @param mixed $value Option value.
479 + * @return array<string, bool|null>
480 + */
481 + private static function noindex_fingerprint($value): array {
482 + if (!is_array($value)) {
483 + return [];
484 + }
485 +
486 + $decisions = [];
487 +
488 + if (array_key_exists('noindex', $value) && !is_array($value['noindex'])) {
489 + $decisions['*'] = !empty($value['noindex']);
490 + }
491 +
492 + foreach ($value as $key => $row) {
493 + if (!is_array($row)) {
494 + continue;
495 + }
496 +
497 + $robots = $row['robots_meta'] ?? null;
498 +
499 + $decisions[(string) $key] = !empty($row['robots_meta_enabled']) && is_array($robots) && array_key_exists('noindex', $robots)
500 + ? !empty($robots['noindex'])
501 + : null;
502 + }
503 +
504 + ksort($decisions);
505 +
506 + return $decisions;
507 + }
508 +
509 + /**
416 510 * The generator the content-change listeners share.
417 511 *
418 512 * @since 2.10.1
419 513 * @return self
@@ -1063,12 +1157,12 @@
1063 1157 continue;
1064 1158 }
1065 1159
1066 1160 foreach ($terms as $term) {
1067 - // A term the user marked noindex must not be advertised in the
1068 - // sitemap: the robots tag now honours term meta, so listing it
1069 - // here would have the sitemap contradict the page's own tag.
1070 - if ($this->term_is_noindexed((int) $term->term_id)) {
1161 + // A term whose archive says noindex (its own override or its
1162 + // taxonomy's), or that redirects, must not be advertised: the
1163 + // sitemap would contradict the page's own signal (#911).
1164 + if (!Indexability::is_indexable_term($term)) {
1071 1165 continue;
1072 1166 }
1073 1167
1074 1168 $url = get_term_link($term);
@@ -1373,14 +1467,15 @@
1373 1467 if (is_wp_error($all_terms)) {
1374 1468 return [];
1375 1469 }
1376 1470
1377 - // Drop terms the user marked noindex. This path feeds the single general
1378 - // sitemap while collect_taxonomy_entries_iter() feeds the segmented ones,
1379 - // so both need the filter or the two disagree about the same term.
1471 + // Drop terms that are not indexable destinations. This path feeds the
1472 + // single general sitemap while collect_taxonomy_entries_iter() feeds
1473 + // the segmented ones, so both need the filter or the two disagree about
1474 + // the same term.
1380 1475 $all_terms = array_values(array_filter(
1381 1476 $all_terms,
1382 - fn($term) => !$this->term_is_noindexed((int) $term->term_id)
1477 + static fn($term) => $term instanceof \WP_Term && Indexability::is_indexable_term($term)
1383 1478 ));
1384 1479
1385 1480 // Group terms by taxonomy
1386 1481 return $this->group_terms_by_taxonomy($all_terms);
@@ -1562,17 +1657,16 @@
1562 1657 if (!in_array($post->post_status, ['publish', 'private'], true)) {
1563 1658 return false;
1564 1659 }
1565 1660
1566 - // Check if post overrides robots and sets noindex.
1567 - if ((bool) get_post_meta($post->ID, '_thinkrank_robots_meta_enabled', true)) {
1568 - $raw = get_post_meta($post->ID, '_thinkrank_robots_meta', true);
1569 - if (is_string($raw) && $raw !== '') {
1570 - $robots = json_decode($raw, true);
1571 - if (is_array($robots) && !empty($robots['noindex'])) {
1572 - return false;
1573 - }
1574 - }
1661 + // A URL whose page says noindex, or that ThinkRank redirects, is not a
1662 + // destination. Only the per-post noindex used to be read here, so a
1663 + // post type set to No-index still had every item listed, and redirected
1664 + // posts were submitted as "Page with redirect" (#911). The password
1665 + // check stays with the setting above: listing protected posts is a
1666 + // choice this sitemap has always offered.
1667 + if (Indexability::is_post_noindexed($post) || Indexability::is_post_redirected($post)) {
1668 + return false;
1575 1669 }
1576 1670
1577 1671 return true;
1578 1672 }
@@ -1611,35 +1705,8 @@
1611 1705 return $ids;
1612 1706 }
1613 1707
1614 1708 /**
1615 - * Whether a term carries an explicit noindex override.
1616 - *
1617 - * Mirrors the post-side check in should_include_post(); terms store the same
1618 - * `_thinkrank_robots_meta_enabled` / `_thinkrank_robots_meta` keys, written
1619 - * by the update-term-seo ability and by the SEO importer.
1620 - *
1621 - * @since 1.31.0
1622 - *
1623 - * @param int $term_id Term to test.
1624 - * @return bool True when the term is marked noindex.
1625 - */
1626 - private function term_is_noindexed(int $term_id): bool {
1627 - if (!(bool) get_term_meta($term_id, '_thinkrank_robots_meta_enabled', true)) {
1628 - return false;
1629 - }
1630 -
1631 - $raw = get_term_meta($term_id, '_thinkrank_robots_meta', true);
1632 - if (!is_string($raw) || $raw === '') {
1633 - return false;
1634 - }
1635 -
1636 - $robots = json_decode($raw, true);
1637 -
1638 - return is_array($robots) && !empty($robots['noindex']);
1639 - }
1640 -
1641 - /**
1642 1709 * Count total URLs in sitemap
1643 1710 *
1644 1711 * @since 1.0.0
1645 1712 *
@@ -2408,12 +2475,13 @@
2408 2475 if ($usage > $limit - $headroom) {
2409 2476 throw new \Error(
2410 2477 sprintf(
2411 2478 /* translators: 1: memory in use, 2: PHP memory limit. */
2412 - __('The sitemap rebuild was stopped at %1$s of the %2$s PHP memory limit, before PHP would have run out of memory. It will be retried by a process with more memory (WP-CLI or a system cron). To let it finish in the admin, raise the PHP memory_limit.', 'thinkrank'),
2413 - size_format($usage),
2414 - size_format($limit)
2479 + esc_html__('The sitemap rebuild was stopped at %1$s of the %2$s PHP memory limit, before PHP would have run out of memory. It will be retried by a process with more memory (WP-CLI or a system cron). To let it finish in the admin, raise the PHP memory_limit.', 'thinkrank'),
2480 + esc_html((string) size_format($usage)),
2481 + esc_html((string) size_format($limit))
2415 2482 ),
2483 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- an integer class constant, not output.
2416 2484 self::MEMORY_ABORT_CODE
2417 2485 );
2418 2486 }
2419 2487 }