/ Hreflang Fixes / Self-Reference

How to Fix Missing Hreflang Self-Reference

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.

1. The self-reference rule

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.

2. The full cluster pattern

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

3. Audit current self-references

Step 1
Run the Hreflang Checker
Self-reference findings list every page that doesn't include its own URL in its hreflang tags.
Step 2
Spot check
# 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.

4. Common causes

Cause 1: "Other languages" plugin

Some CMS plugins generate hreflang for "languages other than current" — sensible UX language but wrong technical output. Self-reference must be in the list.

Cause 2: Conditional template skipping self

// 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

Cause 3: Hand-coded per page

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.

5. Fix patterns per platform

WordPress (WPML / Polylang)

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

Next.js

// 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/'
      }
    }
  };
}

Astro

---
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 -->

6. CI validation

// 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);
}

7. Verify the fix

Step 1
Each variant lists itself
curl + grep each variant. Self URL should appear in the hreflang list.
Step 2
Re-run Hreflang Checker
Missing self-reference findings clear. All cluster pages report complete annotations.
💡 The cluster pattern fixes self-reference by construction. If every page outputs the FULL cluster (the same data on every variant), self-reference is automatic. The variant for any locale always includes its own entry because the data structure includes every entry.

🌍 Re-run the Hreflang Checker

Verify every variant has self-reference.

Run Hreflang Checker →
Related Guides: Hreflang Fixes  ·  Fix Return Tags  ·  Fix Canonical Conflict  ·  Hreflang Guide
💬 Got a problem?