PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.2
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 +116 -46 2.14.0 → 2.14.2 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
@@ -806,9 +900,12 @@
806 900
807 901 // Add image entries if provided
808 902 foreach ($images as $image) {
809 903 $xml .= " <image:image>\n";
810 - $xml .= " <image:loc>" . esc_url(Url_Scheme::apply((string) $image['url'])) . "</image:loc>\n";
904 + // The sitemap protocol wants an escaped URL, and WordPress hands back
905 + // attachment URLs with non-ASCII filenames unencoded (esc_url() does
906 + // not encode them either), so write the percent-encoded form (#924).
907 + $xml .= " <image:loc>" . esc_url(\ThinkRank\Core\Url_Validator::to_ascii(Url_Scheme::apply((string) $image['url']))) . "</image:loc>\n";
811 908
812 909 if (!empty($image['title'])) {
813 910 $xml .= " <image:title>" . esc_html($image['title']) . "</image:title>\n";
814 911 }
@@ -1063,12 +1160,12 @@
1063 1160 continue;
1064 1161 }
1065 1162
1066 1163 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)) {
1164 + // A term whose archive says noindex (its own override or its
1165 + // taxonomy's), or that redirects, must not be advertised: the
1166 + // sitemap would contradict the page's own signal (#911).
1167 + if (!Indexability::is_indexable_term($term)) {
1071 1168 continue;
1072 1169 }
1073 1170
1074 1171 $url = get_term_link($term);
@@ -1256,9 +1353,9 @@
1256 1353 if (preg_match('/src=["\']([^"\']+)["\']/', $img_tag, $src_match)) {
1257 1354 $image_url = $src_match[1];
1258 1355
1259 1356 // Skip if not a valid URL or external image
1260 - if (!filter_var($image_url, FILTER_VALIDATE_URL)) {
1357 + if (!\ThinkRank\Core\Url_Validator::is_valid($image_url)) {
1261 1358 continue;
1262 1359 }
1263 1360
1264 1361 // Extract title and alt attributes
@@ -1373,14 +1470,15 @@
1373 1470 if (is_wp_error($all_terms)) {
1374 1471 return [];
1375 1472 }
1376 1473
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.
1474 + // Drop terms that are not indexable destinations. This path feeds the
1475 + // single general sitemap while collect_taxonomy_entries_iter() feeds
1476 + // the segmented ones, so both need the filter or the two disagree about
1477 + // the same term.
1380 1478 $all_terms = array_values(array_filter(
1381 1479 $all_terms,
1382 - fn($term) => !$this->term_is_noindexed((int) $term->term_id)
1480 + static fn($term) => $term instanceof \WP_Term && Indexability::is_indexable_term($term)
1383 1481 ));
1384 1482
1385 1483 // Group terms by taxonomy
1386 1484 return $this->group_terms_by_taxonomy($all_terms);
@@ -1562,17 +1660,16 @@
1562 1660 if (!in_array($post->post_status, ['publish', 'private'], true)) {
1563 1661 return false;
1564 1662 }
1565 1663
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 - }
1664 + // A URL whose page says noindex, or that ThinkRank redirects, is not a
1665 + // destination. Only the per-post noindex used to be read here, so a
1666 + // post type set to No-index still had every item listed, and redirected
1667 + // posts were submitted as "Page with redirect" (#911). The password
1668 + // check stays with the setting above: listing protected posts is a
1669 + // choice this sitemap has always offered.
1670 + if (Indexability::is_post_noindexed($post) || Indexability::is_post_redirected($post)) {
1671 + return false;
1575 1672 }
1576 1673
1577 1674 return true;
1578 1675 }
@@ -1608,35 +1705,8 @@
1608 1705
1609 1706 $this->woocommerce_excluded_page_ids = $ids;
1610 1707
1611 1708 return $ids;
1612 - }
1613 -
1614 - /**
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 1709 }
1640 1710
1641 1711 /**
1642 1712 * Count total URLs in sitemap