| @@ -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 | |