SEO

Locale-Aware SEO and hreflang

⏱ 16 min

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.

What you'll learn in this lesson
What hreflang actually is and where the tags go
link rel="alternate" tags in a page's head, a standard search engines use, documented directly by Google.
Generating hreflang tags from your i18n plugin's own translation data
Reading the real translation relationships WPML or Polylang already track, not a separate custom list.
Why every language version needs to link to every other, including itself
The specific, well-documented mistake that causes search engines to ignore hreflang annotations entirely.
Generating a locale-aware meta title and description, not a literal translation
Asking the model to write for the target locale's search conventions, grounded in the actual translated content.
Prerequisites

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.

hreflang is a standard, not a WordPress or WPML/Polylang feature

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.

Every language version outputs the full set of tags, not just the others
Including a self-referencing hreflang tag pointing at its own URL, this is required, not optional.
Use fully-qualified URLs, including the protocol
A relative path in the href attribute doesn't meet the documented requirement.
Confirm the relationship is symmetric after generating tags on all sides
The loop in Step 2 does this automatically as long as every translated post also runs the same wp_head hook, verify it did.
A translation stuck in draft status breaks its own hreflang set

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

← Preserving Tone and Terminology Across Languages Translating Structured Content and Product Catalogs →