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.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 1.27.0 All 56 releases
← All changes | includes/seo/class-sitemap-generator.php +137 -51 2.11.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 }
@@ -3814,12 +3882,30 @@
3814 3882 $buffer = [];
3815 3883 }
3816 3884 }
3817 3885
3818 - // Flush the trailing partial page, or a single empty page when the
3819 - // type had no entries at all (parity with the previous behavior of
3820 - // always writing at least one page per configured child).
3821 - if (!empty($buffer) || $page === 0) {
3886 + // Flush the trailing partial page.
3887 + //
3888 + // A type that produced nothing writes no page at all in index
3889 + // mode (#836). It used to write one empty urlset and list it in
3890 + // the index, so a crawler was asked to fetch a file that
3891 + // answers with no URLs — Search Console reports an empty
3892 + // sitemap referenced from an index as a warning, and the fetch
3893 + // is wasted on every pass. On this site three of eleven
3894 + // children were empty: a post type with nothing published and
3895 + // two taxonomies with no terms.
3896 + //
3897 + // Single-file mode still writes its one page even when empty,
3898 + // because that file IS the site's /sitemap.xml and a 404 there
3899 + // is worse than an empty urlset. Nothing lists it, so it costs
3900 + // no crawl budget.
3901 + //
3902 + // Skipping the write also keeps the filename out of
3903 + // $results['sitemaps_generated'], which is what
3904 + // prune_orphaned_segments() treats as "written this run" — so
3905 + // a type that empties after previously publishing has its
3906 + // stale file deleted rather than left serving.
3907 + if (!empty($buffer) || ($page === 0 && $index_config === null)) {
3822 3908 $page++;
3823 3909 $this->write_sitemap_page($this->paginate_url($sitemap_config['url'], $page), $this->wrap_urlset($buffer, $settings, $image_ns), $type, count($buffer), $results, $index_children);
3824 3910 }
3825 3911