Every page in a hreflang cluster must list itself among the alternates. The English page declares "I am en, and the French version is at /fr/, and the German version is at /de/". If the English page lists fr and de but omits its own en entry, the cluster is broken — Google can't verify the English page belongs to the set. This guide covers the rule, the generation pattern that prevents it, and the audit.
Every variant page must include a hreflang tag pointing at itself.
<!-- /en/about (English) — RIGHT --> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <!-- self --> <link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" /> <link rel="alternate" hreflang="de" href="https://example.com/de/uber-uns" /> <link rel="alternate" hreflang="x-default" href="https://example.com/en/about" /> <!-- /en/about — WRONG (missing self) --> <link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" /> <link rel="alternate" hreflang="de" href="https://example.com/de/uber-uns" /> <!-- Where is hreflang="en"? -->
Without self-reference, Google sees this page declaring alternates but no confirmation that THIS page is part of the cluster. The annotations become advisory rather than authoritative. Reciprocity from other pages may compensate, but it's fragile — most checkers flag it as an error to fix.
The pattern that prevents missing self-reference: every page outputs the SAME complete cluster, including itself.
// Single source of truth for the cluster
const aboutCluster = {
'en': 'https://example.com/en/about',
'fr': 'https://example.com/fr/a-propos',
'de': 'https://example.com/de/uber-uns',
'x-default': 'https://example.com/en/about'
};
// Every page in the cluster (en, fr, de) renders this same data
function renderHreflang(cluster) {
return Object.entries(cluster).map(([lang, href]) =>
`<link rel="alternate" hreflang="${lang}" href="${href}" />`
).join('\n');
}
// Output identical on every variant — guaranteed self-reference
# Get hreflang from a page URL="https://example.com/en/about" curl -s "$URL" | grep -oE '<link[^>]*hreflang[^>]*>' # Does any of the href attributes equal $URL? If yes — self-reference present. # If no — missing.
Some CMS plugins generate hreflang for "languages other than current" — sensible UX language but wrong technical output. Self-reference must be in the list.
// BAD pattern in template
{cluster.entries().filter(([lang, url]) => lang !== currentLang).map(...)}
// "Don't render the current language" — wrong intent
// Self-reference must always be present
Developer hand-codes hreflang on each variant. On /en/about, they list /fr/ and /de/ but forget to add /en/. Each variant has its own version of this oversight. Hand-coding always drifts.
Both plugins include self-reference by default. If missing, check plugin settings:
WPML → Languages → SEO options - "Add alternate links" enabled - "Include current language" enabled (some versions hide this) Polylang → Languages → Settings - hreflang generation enabled - Defaults include self-reference
// app/[locale]/page.tsx
export async function generateMetadata({ params }) {
return {
alternates: {
languages: {
'en': 'https://example.com/en/',
'fr': 'https://example.com/fr/',
'de': 'https://example.com/de/',
// Even if currentLocale === 'fr', the 'fr' entry must be present
'x-default': 'https://example.com/en/'
}
}
};
}
---
const allLocales = {
'en': '/en/about',
'fr': '/fr/a-propos',
'de': '/de/uber-uns'
};
const SITE = 'https://example.com';
---
{Object.entries(allLocales).map(([lang, path]) => (
<link rel="alternate" hreflang={lang} href={`${SITE}${path}`} />
))}
<link rel="alternate" hreflang="x-default" href={`${SITE}${allLocales.en}`} />
<!-- Output identical on every locale page — self always present -->
// scripts/check-self-ref.js
const cheerio = require('cheerio');
const fetch = require('node-fetch');
async function checkSelfReference(url) {
const html = await (await fetch(url)).text();
const $ = cheerio.load(html);
const hreflangs = [];
$('link[rel="alternate"][hreflang]').each((i, el) => {
hreflangs.push($(el).attr('href'));
});
if (!hreflangs.includes(url)) {
throw new Error(`Missing self-reference at ${url}. Found: ${hreflangs.join(', ')}`);
}
}
// Run on every variant
for (const url of allLocaleUrls) {
await checkSelfReference(url);
}