PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/integrations/class-multilingual-manager.php +53 -1 1.26.02.7.0 View file →
@@ -80,8 +80,9 @@
80 80 // Keep sitemap queries covering every language. Registered for admin
81 81 // and front end alike: the sitemap can be generated from either.
82 82 add_filter('thinkrank_sitemap_query_args', [$this, 'filter_sitemap_query_args']);
83 83 add_filter('thinkrank_sitemap_term_query_args', [$this, 'filter_sitemap_term_query_args']);
84 + add_filter('thinkrank_sitemap_post_permalink', [$this, 'localize_sitemap_permalink'], 10, 2);
84 85
85 86 if (!is_admin()) {
86 87 // WPML prints at wp_head priority 1 and Polylang at 10, so run
87 88 // after both: by then we know whether anything was printed.
@@ -322,14 +323,17 @@
322 323 *
323 324 * @return array<int, array{code: string, locale: string, url: string, is_default: bool}>
324 325 */
325 326 private function get_wpml_alternates(): array {
327 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML/core hook, not ours to name.
326 328 $languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 1]);
327 329 if (!is_array($languages) || empty($languages)) {
328 330 return [];
329 331 }
330 332
333 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML/core hook, not ours to name.
331 334 $default = (string) apply_filters('wpml_default_language', null);
335 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML/core hook, not ours to name.
332 336 $current = (string) apply_filters('wpml_current_language', null);
333 337 $out = [];
334 338
335 339 foreach ($languages as $language) {
@@ -445,9 +449,11 @@
445 449 $default = (string) ($settings['default-language'] ?? '');
446 450
447 451 // TranslatePress tracks the language being rendered on a global rather
448 452 // than through an accessor.
453 + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- TranslatePress' own global; the name is theirs.
449 454 global $TRP_LANGUAGE;
455 + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- TranslatePress' own global; the name is theirs.
450 456 $current = is_string($TRP_LANGUAGE) ? $TRP_LANGUAGE : '';
451 457
452 458 $out = [];
453 459
@@ -593,8 +599,54 @@
593 599 *
594 600 * @param array<string, mixed> $args Query args.
595 601 * @return array<string, mixed>
596 602 */
603 + /**
604 + * Resolve a sitemap entry's permalink in the post's own language.
605 + *
606 + * The sitemap query runs with suppress_filters pinned (see
607 + * filter_sitemap_query_args) so every language's rows are fetched — but
608 + * that also strips WPML's chance to contextualise the permalink, and the
609 + * debounced cron rebuild runs with no language context at all. Each
610 + * translation therefore resolved to the default-language URL: N sitemap
611 + * entries with different lastmod and images sharing one identical <loc>
612 + * (#409).
613 + *
614 + * WPML's stateless conversion API fixes it per row: look up the post's
615 + * own language, then ask wpml_permalink for the URL in that language.
616 + * Both are documented WPML hooks and no-op safely when absent. Polylang
617 + * needs none of this — its permalink filtering rides post_link, which
618 + * get_permalink() applies regardless of suppress_filters — and
619 + * TranslatePress translates rendered output without duplicating posts.
620 + *
621 + * @since 2.0.1
622 + * @param string $url Permalink as WordPress resolved it.
623 + * @param \WP_Post $post Post the entry describes.
624 + * @return string
625 + */
626 + public function localize_sitemap_permalink(string $url, \WP_Post $post): string {
627 + if ($this->provider !== 'wpml') {
628 + return $url;
629 + }
630 +
631 + // WPML's own documented filters; the prefix rule does not apply to a
632 + // third-party hook we are consuming rather than declaring.
633 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
634 + $lang = apply_filters('wpml_element_language_code', null, [
635 + 'element_id' => $post->ID,
636 + 'element_type' => 'post_' . $post->post_type,
637 + ]);
638 +
639 + if (!is_string($lang) || $lang === '') {
640 + return $url;
641 + }
642 +
643 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
644 + $localized = apply_filters('wpml_permalink', $url, $lang, true);
645 +
646 + return is_string($localized) && $localized !== '' ? $localized : $url;
647 + }
648 +
597 649 public function filter_sitemap_query_args(array $args): array {
598 650 if ($this->provider === 'polylang') {
599 651 // Polylang filters through parse_query, which suppress_filters does
600 652 // not bypass. An empty language disables its language clause.
@@ -618,8 +670,9 @@
618 670 //
619 671 // Measured against WPML 4.9.5: setting suppress_filters => false cut the
620 672 // sitemap down to the active language, and a 'lang' => 'all' argument
621 673 // was ignored outright, so neither is used here.
674 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Dropping it narrows the sitemap to the active language under WPML; see the note above.
622 675 $args['suppress_filters'] = true;
623 676
624 677 return $args;
625 678 }
@@ -640,6 +693,5 @@
640 693 // so there is nothing to add for it here. TranslatePress does not
641 694 // duplicate terms per language at all, so likewise nothing to do.
642 695 return $args;
643 696 }
644 -
645 697 }