From 10a6d6893be4f08f80861707d8841975f354c49c Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 15 Jun 2026 23:03:04 +0300 Subject: [PATCH] Preserve Raw ML values in Yoast indexables Store multilingual Raw ML values in Yoast SEO indexables for posts, custom post types, products, and taxonomy terms. Copy explicitly configured SEO, Open Graph, X/Twitter, and breadcrumb fields directly from the original post or term metadata without applying the current language. Keep empty SEO and social fields null so Yoast templates and its normal fallback hierarchy continue to work. When no custom breadcrumb title is configured, use the Raw ML post title or reconstructed Raw ML term name as the breadcrumb fallback. This prevents Yoast indexables from permanently storing only the language that was active when an object was saved or reindexed. --- src/modules/wp-seo/wp-seo-admin.php | 173 ++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/modules/wp-seo/wp-seo-admin.php b/src/modules/wp-seo/wp-seo-admin.php index 3d411d82..d7b2a371 100644 --- a/src/modules/wp-seo/wp-seo-admin.php +++ b/src/modules/wp-seo/wp-seo-admin.php @@ -21,3 +21,176 @@ function qtranxf_wpseo_load_admin_page_config( array $page_configs ): array { return $page_configs; } + +/** + * Store Raw ML values in Yoast indexables. + * + * Explicit Yoast fields are stored as Raw ML. + * Empty fields remain null so Yoast templates and fallbacks keep working. + * Breadcrumb title falls back to the Raw ML object title. + */ +function qtranxf_wpseo_save_raw_indexable_fields( $indexable ): void { + if ( + ! is_object( $indexable ) || + empty( $indexable->object_id ) + ) { + return; + } + + $values = array(); + $changed = false; + + switch ( $indexable->object_type ) { + case 'post': + $post_id = (int) $indexable->object_id; + $post = get_post( $post_id ); + + if ( ! $post instanceof WP_Post ) { + return; + } + + $meta_map = array( + '_yoast_wpseo_title' + => 'title', + + '_yoast_wpseo_metadesc' + => 'description', + + '_yoast_wpseo_bctitle' + => 'breadcrumb_title', + + '_yoast_wpseo_opengraph-title' + => 'open_graph_title', + + '_yoast_wpseo_opengraph-description' + => 'open_graph_description', + + '_yoast_wpseo_twitter-title' + => 'twitter_title', + + '_yoast_wpseo_twitter-description' + => 'twitter_description', + ); + + foreach ( $meta_map as $meta_key => $field ) { + $value = get_post_meta( + $post_id, + $meta_key, + true + ); + + $values[ $field ] = ( + is_string( $value ) && + $value !== '' + ) ? $value : null; + } + + /* + * Breadcrumb title is the only field that must fall + * back to the untranslated database object title. + * + * Other empty fields remain null so Yoast can use its + * templates and normal fallback hierarchy. + */ + if ( $values['breadcrumb_title'] === null ) { + $values['breadcrumb_title'] = wp_strip_all_tags( + $post->post_title, + true + ); + } + break; + + case 'term': + $term_id = (int) $indexable->object_id; + $taxonomy = $indexable->object_sub_type; + $term = get_term( $term_id, $taxonomy ); + + if ( ! $term instanceof WP_Term ) { + return; + } + + $term_meta = array(); + + if ( class_exists( 'WPSEO_Taxonomy_Meta' ) ) { + $loaded_meta = WPSEO_Taxonomy_Meta::get_term_meta( + $term, + $taxonomy, + null + ); + + if ( is_array( $loaded_meta ) ) { + $term_meta = $loaded_meta; + } + } + + $meta_map = array( + 'wpseo_title' + => 'title', + + 'wpseo_desc' + => 'description', + + 'wpseo_bctitle' + => 'breadcrumb_title', + + 'wpseo_opengraph-title' + => 'open_graph_title', + + 'wpseo_opengraph-description' + => 'open_graph_description', + + 'wpseo_twitter-title' + => 'twitter_title', + + 'wpseo_twitter-description' + => 'twitter_description', + ); + + foreach ( $meta_map as $meta_key => $field ) { + $value = $term_meta[ $meta_key ] ?? null; + + $values[ $field ] = ( + is_string( $value ) && + $value !== '' + ) ? $value : null; + } + + if ( $values['breadcrumb_title'] === null ) { + if ( function_exists( 'qtranxf_get_term_joined' ) ) { + $term = qtranxf_get_term_joined( + $term, + $taxonomy + ); + } + + $values['breadcrumb_title'] = $term->name; + } + break; + + default: + return; + } + + foreach ( $values as $field => $value ) { + if ( $indexable->{$field} === $value ) { + continue; + } + + $indexable->{$field} = $value; + $changed = true; + } + + if ( $changed ) { + /* + * Direct model save does not trigger the + * wpseo_saved_indexable action recursively. + */ + $indexable->save(); + } +} + +add_action( + 'wpseo_saved_indexable', + 'qtranxf_wpseo_save_raw_indexable_fields', + 10 +);