Registering a translation inside WPML or Polylang, the work of Lessons 2 and 3, gives search
engines a data source to draw from, but it doesn’t automatically tell them how the language
versions of a page relate to each other on the page itself. That’s what hreflang is for, a
standard, well-documented mechanism, not a WordPress-specific one, that search engines use to
understand which URL serves which language and region. This lesson covers generating those tags
correctly from your plugin’s own translation data, then goes a step further: a meta title or
description that’s simply translated word for word often reads awkwardly, or misses how people in
that locale actually phrase a search, so the second half of this lesson generates locale-aware
metadata instead.
Lesson 2 or Lesson 3, this lesson reads the same translation relationships those abilities
create, whichever plugin you’re running. A theme or plugin setup where you can hook into
wp_head, most standard WordPress themes support this without modification.
Step 1: What hreflang actually is
An hreflang annotation is a <link> tag placed inside a page’s <head>, in the form
<link rel="alternate" hreflang="x" href="url" />, telling a search engine that a given URL is an
alternate version of the current page in a different language or region. Google’s own
documentation on localized versions describes this mechanism plainly: it’s how you tell Google
about localized versions of your page so it can serve the right one to the right searcher, it
doesn’t directly affect ranking so much as it affects which of your pages gets shown for a
given user’s language and region.
Nothing about the hreflang mechanism itself is specific to WordPress. WPML and Polylang both
happen to make it straightforward to generate because they already track which posts are
translations of each other, but the annotation format and the search engines that read it are the
same regardless of what generated the tags.
Step 2: Generate the tags from your plugin’s own translation data
Rather than maintaining a separate list of language relationships, read the same data WPML or Polylang already track, whichever one your site runs.
// File: locale-seo/locale-seo.php
add_action( 'wp_head', 'locale_seo_output_hreflang_tags' );
function locale_seo_output_hreflang_tags() {
if ( ! is_singular() ) {
return;
}
$post_id = get_queried_object_id();
$urls = array();
if ( function_exists( 'apply_filters' ) && has_filter( 'wpml_active_languages' ) ) {
// WPML: wpml_object_id resolves this post's ID in every active language.
$languages = apply_filters( 'wpml_active_languages', null, array() );
foreach ( $languages as $lang_code => $language ) {
$translated_id = apply_filters( 'wpml_object_id', $post_id, get_post_type( $post_id ), false, $lang_code );
if ( $translated_id ) {
$urls[ $lang_code ] = get_permalink( $translated_id );
}
}
} elseif ( function_exists( 'pll_get_post_translations' ) ) {
// Polylang: pll_get_post_translations() already returns every language, including this one.
foreach ( pll_get_post_translations( $post_id ) as $lang_code => $translated_id ) {
$urls[ $lang_code ] = get_permalink( $translated_id );
}
}
// A page always links to itself too, this is part of what makes hreflang bidirectional.
foreach ( $urls as $lang_code => $url ) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
esc_attr( $lang_code ),
esc_url( $url )
);
}
}
Step 3: The mistake that makes search engines ignore the whole set
Google’s own guidance is explicit about a specific requirement: if page A links to page B as an alternate, page B must link back to page A, and to every other language version in the same set, including itself. Miss that, and Google’s documentation states plainly that it may ignore the annotations rather than partially apply them.
Lesson 2 and 3’s abilities always create translations as draft, correctly, so a human can review
them first. But a draft post’s permalink typically isn’t the public URL a search engine would
crawl. Don’t run this hreflang output against draft translations, wait until a translation is
reviewed and published (Lesson 7 covers exactly that gate) before it’s included in the set, an
incomplete or draft-linked set is arguably worse than none at all.
Step 4: Generate a locale-aware meta title and description, not a literal translation
A meta title translated word for word from the source language can read stiffly, or simply miss how people phrase the same search in the target locale. This step asks the model to write new, locale-appropriate metadata grounded in the already-translated content, rather than translating the source language’s existing meta title directly.
// File: locale-seo/locale-seo.php
function locale_seo_generate_metadata( $translated_post_id, $target_language ) {
$post = get_post( $translated_post_id );
if ( ! $post ) {
return;
}
$body = wp_strip_all_tags( $post->post_content );
$prompt = <<<PROMPT
This is a post already translated into the language identified by "{$target_language}". Write a
meta title (under 60 characters) and a meta description (under 155 characters) for it, written
the way a native speaker of that language would naturally phrase a search-facing title and
summary, not a literal re-translation of the title below. Base both only on the content given.
Title: {$post->post_title}
Content: {$body}
Return the meta title on the first line prefixed with "Title: " and the meta description on the
second line prefixed with "Description: ".
PROMPT;
$response = wp_ai_client_prompt( $prompt )->generate_text();
if ( preg_match( '/^Title:\s*(.+)$/mi', $response, $title_match )
&& preg_match( '/^Description:\s*(.+)$/mi', $response, $desc_match ) ) {
update_post_meta( $translated_post_id, '_seo_meta_description', sanitize_text_field( trim( $desc_match[1] ) ) );
update_post_meta( $translated_post_id, '_seo_meta_title', sanitize_text_field( trim( $title_match[1] ) ) );
}
}
_seo_meta_description reuses the same key Course 6’s meta description project wrote to, kept
consistent here since each translated post is its own post ID with its own meta, no per-language
suffix is needed on the key itself.
Test it
wp-env run cli wp eval '
locale_seo_generate_metadata( 55, "es" );
'
wp-env run cli wp post meta get 55 _seo_meta_title
wp-env run cli wp post meta get 55 _seo_meta_description
Read the generated title and description as a native speaker of the target language would, ask whether it reads like something a person in that locale would naturally search for, not just whether it’s an accurate translation of the source title.
Recap
hreflang is a standard, not a WordPress feature, <link rel="alternate" hreflang="x"> tags in
a page’s <head>, generated here from the same translation relationships WPML’s wpml_object_id
or Polylang’s pll_get_post_translations() already track, and every language version must link to
every other one, including itself, or search engines may ignore the whole set. Locale-aware
metadata goes a step further than translation, asking the model to write a meta title and
description the way a native speaker in that locale would phrase them, grounded in the translated
content rather than a literal re-translation of the source.
Resources & further reading
- Tell Google about localized versions of your page, Google Search Central
- wpml_object_id, wpml.org
- Polylang function reference, polylang.pro
- Project: Meta Description and Alt-Text Generator, this track, Course 6