EnjoySEOs Header
EnjoySEOs
Free SEO toolkit

Hreflang Checker

Audit the hreflang annotations on any page or XML sitemap. Detect missing self-references, invalid language tags, duplicate mappings and broken return links — entirely in your browser.

All parsing and validation runs locally. Nothing is uploaded or proxied.

Pages on other domains are often blocked by CORS. If the fetch fails, paste the page source into the “Pasted HTML” tab instead. This tool never proxies requests through a server.

\n' + '\n' + '

Sample page

\n' + '

This is a demonstration page used to showcase the checker.

\n' + '\n' + ''; var SAMPLE_XML = '\n' + '\n' + ' \n' + ' https://example.com/\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' https://example.com/fr/\n' + ' \n' + ' \n' + ' \n' + ' \n' + ''; var SAMPLE_BULK = 'https://example.com/\n' + 'https://example.com/fr/\n' + 'https://example.com/de/'; /* ------------------------------------------------------------------ * * Input handlers — Sample / Clear * ------------------------------------------------------------------ */ on($('ehc-htmlSample'), 'click', function(){ $('ehc-html').value = SAMPLE_HTML; $('ehc-htmlUrl').value = 'https://example.com/'; if($('ehc-autoHtml').checked) analyzeHtmlNow(); showMsg(['Sample HTML loaded. It is demonstration content and contains deliberate issues so you can see the checker in action.'], 'Note:', 'ehc-warnbox'); }); on($('ehc-htmlClear'), 'click', function(){ $('ehc-html').value = ''; $('ehc-htmlUrl').value = ''; clearResults(); clearMsg(); }); on($('ehc-xmlSample'), 'click', function(){ $('ehc-xml').value = SAMPLE_XML; showMsg(['Sample sitemap loaded. It is demonstration content only.'], 'Note:', 'ehc-warnbox'); }); on($('ehc-xmlClear'), 'click', function(){ $('ehc-xml').value = ''; clearResults(); clearMsg(); }); on($('ehc-bulkSample'), 'click', function(){ $('ehc-bulk').value = SAMPLE_BULK; showMsg(['Sample URL list loaded. These URLs use example.com and cannot be fetched, which demonstrates the CORS handling.'], 'Note:', 'ehc-warnbox'); }); on($('ehc-bulkClear'), 'click', function(){ $('ehc-bulk').value = ''; clearResults(); clearMsg(); }); /* ------------------------------------------------------------------ * * Clear results * ------------------------------------------------------------------ */ function clearResults(){ state.analyses = []; state.collapsed = {}; $('ehc-results').hidden = true; $('ehc-findings').textContent = ''; $('ehc-stats').textContent = ''; $('ehc-scorebox').textContent = ''; $('ehc-runInfo').textContent = ''; hideBulkProgress(); } on($('ehc-reset'), 'click', function(){ state.jobToken++; if(state.bulkAbort){ try{ state.bulkAbort.abort(); }catch(e){} state.bulkAbort = null; } state.bulkRunning = false; clearResults(); clearMsg(); $('ehc-url').value = ''; $('ehc-html').value = ''; $('ehc-htmlUrl').value = ''; $('ehc-xml').value = ''; $('ehc-bulk').value = ''; $('ehc-urlCancel').hidden = true; $('ehc-bulkCancel').hidden = true; $('ehc-errorsOnly').checked = false; $('ehc-showInfo').checked = true; state.errorsOnly = false; state.showInfo = true; }); /* ------------------------------------------------------------------ * * Live filters * ------------------------------------------------------------------ */ on($('ehc-showInfo'), 'change', function(){ state.showInfo = this.checked; renderResults(); }); on($('ehc-errorsOnly'), 'change', function(){ state.errorsOnly = this.checked; renderResults(); }); /* ------------------------------------------------------------------ * * Mode A — Live URL * ------------------------------------------------------------------ */ var urlAbort = null; on($('ehc-checkUrl'), 'click', async function(){ var url = ($('ehc-url').value || '').trim(); if(!url){ showMsg(['Please enter a page URL.']); return; } if(!/^https?:\/\//i.test(url)) url = 'https://' + url; if(!isValidHttpUrl(url)){ showMsg(['That does not look like a valid http(s) URL.']); return; } clearMsg(); clearResults(); $('ehc-urlCancel').hidden = false; var btn = this; btn.disabled = true; btn.textContent = 'Fetching…'; var token = ++state.jobToken; urlAbort = new AbortController(); try{ var html = await fetchPage(url, urlAbort.signal); if(token !== state.jobToken) return; var a = analyzeHtmlSource(html, url, url); a.fetched = true; state.analyses = [a]; renderResults(); showMsg(['Fetched and analysed ' + url + '.'], 'Done:', 'ehc-okbox'); }catch(err){ if(token !== state.jobToken) return; if(err && err.name === 'AbortError'){ showMsg(['Fetch cancelled.']); } else { var a2 = newAnalysis(url, url); a2.ok = false; a2.errorMessage = fetchErrorMessage(err, url); state.analyses = [a2]; renderResults(); showMsg([a2.errorMessage], 'Could not verify the page:', 'ehc-warnbox'); } } finally { if(token === state.jobToken){ btn.disabled = false; btn.textContent = 'Check URL'; $('ehc-urlCancel').hidden = true; urlAbort = null; } } }); on($('ehc-urlCancel'), 'click', function(){ if(urlAbort){ try{ urlAbort.abort(); }catch(e){} } }); /* ------------------------------------------------------------------ * * Mode B — Pasted HTML * ------------------------------------------------------------------ */ var htmlTimer = null; function analyzeHtmlNow(){ var html = $('ehc-html').value; if(!html.trim()){ clearResults(); clearMsg(); return; } var pageUrl = ($('ehc-htmlUrl').value || '').trim(); if(pageUrl && !/^https?:\/\//i.test(pageUrl)) pageUrl = 'https://' + pageUrl; if(pageUrl && !isValidHttpUrl(pageUrl)) pageUrl = ''; var a = analyzeHtmlSource(html, pageUrl, pageUrl || 'Pasted HTML'); state.analyses = [a]; renderResults(); clearMsg(); } on($('ehc-checkHtml'), 'click', function(){ if(!$('ehc-html').value.trim()){ showMsg(['Paste the page HTML source first.']); return; } analyzeHtmlNow(); }); on($('ehc-html'), 'input', function(){ if(!$('ehc-autoHtml').checked) return; if(htmlTimer) clearTimeout(htmlTimer); htmlTimer = setTimeout(function(){ if($('ehc-html').value.length > 600000) return; // safety analyzeHtmlNow(); }, 450); }); /* ------------------------------------------------------------------ * * Mode C — XML sitemap * ------------------------------------------------------------------ */ on($('ehc-checkXml'), 'click', function(){ var xml = $('ehc-xml').value; if(!xml.trim()){ showMsg(['Paste an XML sitemap first.']); return; } clearMsg(); var a = analyzeSitemapSource(xml, 'Pasted XML sitemap'); state.analyses = [a]; renderResults(); if(!a.ok){ showMsg([a.errorMessage], 'The sitemap could not be analysed:', 'ehc-errs'); } else { var msgs = []; if(a.sitemapTruncated) msgs.push('Only the first ' + a.children.length + ' URLs were analysed (' + a.sitemapTruncated + ' total).'); if(msgs.length) showMsg(msgs, 'Note:', 'ehc-warnbox'); } }); /* ------------------------------------------------------------------ * * Mode D — Bulk URLs * ------------------------------------------------------------------ */ function showBulkProgress(){ $('ehc-bulkProg').hidden = false; } function hideBulkProgress(){ $('ehc-bulkProg').hidden = true; $('ehc-bulkBar').style.setProperty('width', '0%', 'important'); $('ehc-bulkTxt').textContent = ''; } function setBulkProgress(p, txt){ showBulkProgress(); $('ehc-bulkBar').style.setProperty('width', Math.max(0, Math.min(100, p)) + '%', 'important'); $('ehc-bulkTxt').textContent = txt || ''; } function tick(){ return new Promise(function(r){ setTimeout(r, 0); }); } on($('ehc-checkBulk'), 'click', async function(){ var raw = $('ehc-bulk').value || ''; var list = raw.split(/\r?\n/).map(function(x){ return x.trim(); }).filter(Boolean); // Normalize / validate var invalid = []; var urls = []; var seen = new Set(); list.forEach(function(u, i){ if(!/^https?:\/\//i.test(u)) u = 'https://' + u; if(!isValidHttpUrl(u)){ invalid.push('Line ' + (i + 1) + ': "' + list[i] + '" is not a valid http(s) URL.'); return; } var n = normUrl(u); if(seen.has(n)){ invalid.push('Line ' + (i + 1) + ': duplicate URL skipped.'); return; } seen.add(n); urls.push(u); }); if(!urls.length){ showMsg(invalid.length ? invalid : ['Please enter at least one URL, one per line.'], 'Nothing to check:', 'ehc-errs'); return; } if(urls.length > 60){ showMsg(['Only the first 60 URLs will be checked in this run.'], 'Note:', 'ehc-warnbox'); urls = urls.slice(0, 60); } if(invalid.length){ showMsg(invalid, 'Some lines were skipped:', 'ehc-warnbox'); } else { clearMsg(); } clearResults(); state.bulkRunning = true; state.bulkAbort = new AbortController(); var token = ++state.jobToken; var btn = this; btn.disabled = true; $('ehc-bulkCancel').hidden = false; var results = []; var done = 0; var CONCURRENCY = 3; var idx = 0; var cancelled = false; function markCancelled(){ cancelled = true; } on($('ehc-bulkCancel'), 'click', function once(){ markCancelled(); try{ state.bulkAbort.abort(); }catch(e){} }); async function worker(){ while(idx < urls.length && !cancelled){ var myIdx = idx++; var url = urls[myIdx]; if(token !== state.jobToken) return; var entry; try{ var html = await fetchPage(url, state.bulkAbort.signal); if(token !== state.jobToken) return; entry = analyzeHtmlSource(html, url, url); entry.fetched = true; }catch(err){ if(err && err.name === 'AbortError'){ cancelled = true; return; } entry = newAnalysis(url, url); entry.ok = false; entry.errorMessage = fetchErrorMessage(err, url); entry.errorKind = (err && err.kind) || 'UNKNOWN'; } results[myIdx] = entry; done++; setBulkProgress((done / urls.length) * 100, done + ' of ' + urls.length + ' checked…'); await tick(); } } var workers = []; for(var w = 0; w < Math.min(CONCURRENCY, urls.length); w++) workers.push(worker()); try{ await Promise.all(workers); }catch(e){} if(token !== state.jobToken) return; state.bulkRunning = false; state.bulkAbort = null; btn.disabled = false; $('ehc-bulkCancel').hidden = true; hideBulkProgress(); var final = results.filter(Boolean); var fetchedList = final.filter(function(a){ return a.ok; }); runReciprocalChecks(fetchedList); // Informational note about unverifiable pages final.forEach(function(a){ if(!a.ok && a.errorKind === 'CORS'){ a.checks.push(chk('info', 'Return links could not be verified for this page', 'The page source could not be read, so reciprocal hreflang links were not checked. Paste its HTML into the “Pasted HTML” tab to verify locally.')); } }); state.analyses = final; renderResults(); if(cancelled){ showMsg(['Bulk check stopped after ' + final.length + ' of ' + urls.length + ' URLs.'], 'Note:', 'ehc-warnbox'); } else { var okN = final.filter(function(a){ return a.ok; }).length; var failN = final.length - okN; var msgs = ['Checked ' + final.length + ' URL(s). ' + okN + ' analysed successfully, ' + failN + ' could not be read.']; if(failN) msgs.push('Pages that could not be read were NOT treated as missing hreflang. Use the “Pasted HTML” tab for those.'); showMsg(msgs, 'Done:', 'ehc-okbox'); } }); /* ------------------------------------------------------------------ * * Rendering * ------------------------------------------------------------------ */ function renderResults(){ var A = state.analyses; var box = $('ehc-results'); if(!A.length){ box.hidden = true; return; } box.hidden = false; renderDashboard(A); renderFindings(A); renderRunInfo(A); } function renderRunInfo(A){ var totalAlt = 0, totalErr = 0, totalWarn = 0, okN = 0; A.forEach(function(a){ totalAlt += a.alternates.length; totalErr += a.counts.error || 0; totalWarn += a.counts.warning || 0; if(a.ok) okN++; }); var parts = []; parts.push(A.length + ' source' + (A.length === 1 ? '' : 's')); if(okN !== A.length) parts.push(okN + ' readable'); parts.push(totalAlt + ' alternate' + (totalAlt === 1 ? '' : 's')); parts.push(totalErr + ' error' + (totalErr === 1 ? '' : 's')); parts.push(totalWarn + ' warning' + (totalWarn === 1 ? '' : 's')); $('ehc-runInfo').textContent = parts.join(' · '); } /* ---------- Aggregate helpers ---------- */ function aggregate(A){ var agg = { alternates: 0, tags: new Set(), urls: new Set(), error: 0, warning: 0, info: 0, pass: 0, ok: 0, failed: 0 }; A.forEach(function(a){ if(a.ok) agg.ok++; else agg.failed++; agg.alternates += a.alternates.length; a.alternates.forEach(function(x){ if(x.tagNorm) agg.tags.add(x.tagNorm); if(x.hrefNorm) agg.urls.add(x.hrefNorm); }); agg.error += a.counts.error || 0; agg.warning += a.counts.warning || 0; agg.info += a.counts.info || 0; agg.pass += a.counts.pass || 0; }); return agg; } /* ---------- Dashboard ---------- */ function renderDashboard(A){ var agg = aggregate(A); // Aggregate score var scores = A.map(function(a){ return a.score; }).filter(function(s){ return s !== null && s !== undefined; }); var score = null, provisional = false; if(scores.length){ var sum = scores.reduce(function(x, y){ return x + y; }, 0); score = Math.round(sum / scores.length); provisional = A.some(function(a){ return a.scoreProvisional; }) || agg.failed > 0; } var box = $('ehc-scorebox'); box.textContent = ''; var numEl = h('div', {class: 'ehc-scorenum'}); if(score === null){ numEl.textContent = '—'; numEl.className = 'ehc-scorenum na'; } else { numEl.textContent = String(score); if(score >= 85) numEl.className = 'ehc-scorenum'; else if(score >= 60) numEl.className = 'ehc-scorenum warn'; else numEl.className = 'ehc-scorenum bad'; } box.appendChild(numEl); box.appendChild(h('div', {class: 'ehc-scorelabel'}, 'Health score')); box.appendChild(h('div', {class: 'ehc-scorenote'}, score === null ? 'No readable source — score unavailable.' : (provisional ? 'Preliminary estimate. Based on this tool\'s checks only, not a Google metric.' : 'Diagnostic estimate only — not a Google ranking factor.') )); // Stats var stats = $('ehc-stats'); stats.textContent = ''; var items = [ [String(agg.alternates), 'Alternate declarations', ''], [String(agg.tags.size), 'Unique language tags', ''], [String(agg.urls.size), 'Unique destination URLs', ''], [String(agg.error), 'Errors', 'err'], [String(agg.warning), 'Warnings', 'warn'], [String(agg.pass), 'Passed checks', 'ok'] ]; items.forEach(function(it){ var cls = 'ehc-stat' + (it[2] ? ' ' + it[2] : ''); stats.appendChild(h('div', {class: cls}, [ h('b', null, it[0]), h('span', null, it[1]) ])); }); } /* ---------- Findings ---------- */ function renderFindings(A){ var box = $('ehc-findings'); box.textContent = ''; if(A.length === 1 && !A[0].isSitemapRoot){ box.appendChild(analysisBlock(A[0], false, 0)); return; } // Sitemap root: render aggregate then children if(A.length === 1 && A[0].isSitemapRoot){ box.appendChild(sitemapSummary(A[0])); A[0].children.forEach(function(child, i){ box.appendChild(analysisBlock(child, true, i)); }); return; } // Multiple sources A.forEach(function(a, i){ box.appendChild(analysisBlock(a, true, i)); }); } function sitemapSummary(a){ var wrap = h('div', {class: 'ehc-findings'}); wrap.appendChild(h('div', {class: 'ehc-grouphead'}, 'Sitemap overview')); wrap.appendChild(h('p', {class: 'ehc-hint'}, a.children.length + ' URL entr' + (a.children.length === 1 ? 'y' : 'ies') + ' analysed from the supplied sitemap.' + (a.sitemapTruncated ? ' The sitemap contained ' + a.sitemapTruncated + ' URLs in total; only the first 200 were checked.' : '') )); return wrap; } function analysisBlock(a, withHeader, index){ var card = h('div', {class: 'ehc-acard'}); if(withHeader) card.setAttribute('data-aid', String(a.id)); if(withHeader){ var head = h('div', {class: 'ehc-acard-head'}); var title = h('div', {class: 'ehc-acard-title'}); title.appendChild(document.createTextNode(a.label || ('Source ' + (index + 1)))); if(a.pageUrl && a.pageUrl !== a.label){ var sub = h('span', {class: 'ehc-hint', style: 'display:block !important;margin-top:2px !important;font-weight:500 !important'}); sub.textContent = a.pageUrl; title.appendChild(sub); } head.appendChild(title); var badge; if(!a.ok) badge = h('span', {class: 'ehc-badge bad'}, 'Unavailable'); else if((a.counts.error || 0) > 0) badge = h('span', {class: 'ehc-badge bad'}, a.counts.error + ' error' + (a.counts.error === 1 ? '' : 's')); else if((a.counts.warning || 0) > 0) badge = h('span', {class: 'ehc-badge warn'}, a.counts.warning + ' warning' + (a.counts.warning === 1 ? '' : 's')); else if(a.alternates.length) badge = h('span', {class: 'ehc-badge ok'}, 'No issues'); else badge = h('span', {class: 'ehc-badge'}, 'No hreflang'); head.appendChild(badge); if(a.score !== null && a.score !== undefined){ head.appendChild(h('span', {class: 'ehc-badge acc'}, 'Score ' + a.score)); } var tg = h('button', {type: 'button', class: 'ehc-tg', 'aria-label': 'Toggle details'}, state.collapsed[a.id] ? '▸' : '▾'); on(tg, 'click', function(){ state.collapsed[a.id] = !state.collapsed[a.id]; card.classList.toggle('collapsed', !!state.collapsed[a.id]); tg.textContent = state.collapsed[a.id] ? '▸' : '▾'; }); head.appendChild(tg); card.appendChild(head); if(state.collapsed[a.id]) card.classList.add('collapsed'); } var body = h('div', {class: 'ehc-acard-body'}); if(!a.ok){ body.appendChild(h('div', {class: 'ehc-errs'}, a.errorMessage || 'This source could not be analysed.')); if(a.errorMessage && /CORS|paste/i.test(a.errorMessage)){ body.appendChild(h('p', {class: 'ehc-hint'}, 'Open the page in your browser, use “View source”, then paste the HTML into the “Pasted HTML” tab. The tool will analyse it locally without any network requests.')); } } else { // Findings body.appendChild(findingsBlock(a)); // Mapping table if(a.alternates.length){ body.appendChild(h('div', {class: 'ehc-grouphead'}, 'Hreflang mapping')); body.appendChild(mappingTable(a)); } else { body.appendChild(h('div', {class: 'ehc-empty'}, 'No hreflang alternate declarations were found in this source.')); } } card.appendChild(body); return card; } function findingsBlock(a){ var wrap = h('div', {class: 'ehc-findings'}); var checks = a.checks.slice(); if(state.errorsOnly){ checks = checks.filter(function(c){ return c.severity === 'error'; }); } else if(!state.showInfo){ checks = checks.filter(function(c){ return c.severity !== 'info'; }); } if(!checks.length){ wrap.appendChild(h('div', {class: 'ehc-empty'}, state.errorsOnly ? 'No errors were detected.' : 'No findings to display with the current filters.')); return wrap; } var order = { error: 0, warning: 1, info: 2, pass: 3 }; checks.sort(function(x, y){ return order[x.severity] - order[y.severity]; }); var groups = { error: [], warning: [], info: [], pass: [] }; checks.forEach(function(c){ groups[c.severity].push(c); }); function group(label, list, cls){ if(!list.length) return; wrap.appendChild(h('div', {class: 'ehc-grouphead ' + cls}, label + ' (' + list.length + ')')); list.forEach(function(c){ wrap.appendChild(checkRow(c)); }); } group('Errors', groups.error, 'ehc-error'); group('Warnings', groups.warning, 'ehc-warning'); group('Recommendations', groups.info, 'ehc-info'); group('Passed checks', groups.pass, 'ehc-pass'); return wrap; } function severityIcon(sev){ if(sev === 'error') return '×'; if(sev === 'warning') return '!'; if(sev === 'info') return 'i'; return '✓'; } function checkRow(c){ var row = h('div', {class: 'ehc-check ehc-' + c.severity}); row.appendChild(h('span', {class: 'ehc-check-icon', 'aria-hidden': 'true'}, severityIcon(c.severity))); var body = h('div', {class: 'ehc-check-body'}); body.appendChild(h('div', {class: 'ehc-check-title'}, c.title)); if(c.detail) body.appendChild(h('div', {class: 'ehc-check-detail'}, c.detail)); if(c.tag || c.url){ var meta = h('div', {class: 'ehc-check-meta'}); if(c.tag) meta.appendChild(h('code', null, c.tag)); if(c.url) meta.appendChild(h('code', null, c.url)); body.appendChild(meta); } if(c.fix){ var fix = h('div', {class: 'ehc-check-fix'}); fix.appendChild(h('strong', null, 'Suggested fix: ')); fix.appendChild(document.createTextNode(c.fix)); body.appendChild(fix); } row.appendChild(body); return row; } /* ---------- Mapping table ---------- */ function mappingTable(a){ var wrap = h('div', {class: 'ehc-tablewrap'}); var table = h('table', {class: 'ehc-table'}); var thead = h('thead'); var htr = h('tr'); ['Language', 'Destination URL', 'Host', 'Duplicates', 'Self-ref', 'Status', 'Suggested fix'].forEach(function(label){ htr.appendChild(h('th', {scope: 'col'}, label)); }); thead.appendChild(htr); table.appendChild(thead); var tbody = h('tbody'); a.alternates.forEach(function(alt){ var tr = h('tr'); var tdTag = h('td', {class: 'ehc-tag'}); tdTag.textContent = alt.raw.tag || '(missing)'; tr.appendChild(tdTag); var tdUrl = h('td', {class: 'ehc-url'}); tdUrl.textContent = alt.raw.href || '(missing)'; tr.appendChild(tdUrl); var tdHost = h('td'); tdHost.textContent = alt.host || '—'; tr.appendChild(tdHost); var tdDup = h('td'); tdDup.textContent = alt.dupStatus || '—'; tr.appendChild(tdDup); var tdSelf = h('td'); tdSelf.textContent = alt.selfStatus || '—'; tr.appendChild(tdSelf); var tdSt = h('td', {class: 'ehc-status'}); if(alt.issues && alt.issues.length){ tdSt.appendChild(h('span', {class: 'ehc-badge bad'}, 'Error')); } else { tdSt.appendChild(h('span', {class: 'ehc-badge ok'}, 'OK')); } tr.appendChild(tdSt); var tdFix = h('td'); tdFix.textContent = alt.fix || '—'; tr.appendChild(tdFix); tbody.appendChild(tr); }); table.appendChild(tbody); wrap.appendChild(table); return wrap; } /* ------------------------------------------------------------------ * * Export builders * ------------------------------------------------------------------ */ function csvCell(v){ v = (v === null || v === undefined) ? '' : String(v); if(/[",\r\n]/.test(v)) return '"' + v.replace(/"/g, '""') + '"'; return v; } function collectAllAlternates(){ var out = []; state.analyses.forEach(function(a){ if(a.isSitemapRoot){ a.children.forEach(function(c){ c.alternates.forEach(function(x){ out.push({ a: c, alt: x }); }); }); } else { a.alternates.forEach(function(x){ out.push({ a: a, alt: x }); }); } }); return out; } function buildTagsSnippet(){ var pairs = collectAllAlternates(); if(!pairs.length) return ''; return pairs.map(function(p){ var tag = p.alt.raw.tag || ''; var href = p.alt.raw.href || ''; return ''; }).join('\n'); } function buildFixedSnippet(){ var pairs = collectAllAlternates(); if(!pairs.length) return ''; var lines = []; var seen = new Set(); pairs.forEach(function(p){ var tag = p.alt.tagNorm || p.alt.raw.tag || ''; var href = p.alt.hrefNorm || p.alt.raw.href || ''; if(!tag || !href) return; var key = tag + '|' + href; if(seen.has(key)) return; seen.add(key); lines.push(''); }); return lines.join('\n'); } function buildReport(){ var lines = []; lines.push('Hreflang Checker report'); lines.push('Generated: ' + new Date().toISOString()); lines.push('Sources analysed: ' + state.analyses.length); lines.push(''); var agg = aggregate(state.analyses); lines.push('Aggregate'); lines.push(' Alternate declarations : ' + agg.alternates); lines.push(' Unique language tags : ' + agg.tags.size); lines.push(' Unique destination URLs: ' + agg.urls.size); lines.push(' Errors : ' + agg.error); lines.push(' Warnings : ' + agg.warning); lines.push(' Recommendations : ' + agg.info); lines.push(' Passed checks : ' + agg.pass); lines.push(''); state.analyses.forEach(function(a){ lines.push('================================================'); lines.push('Source: ' + (a.label || '')); if(a.pageUrl && a.pageUrl !== a.label) lines.push('URL : ' + a.pageUrl); if(a.canonical) lines.push('Canonical: ' + a.canonical); if(!a.ok){ lines.push('STATUS: NOT ANALYSED'); lines.push(' ' + (a.errorMessage || 'The source could not be read.')); lines.push(''); return; } if(a.score !== null && a.score !== undefined) lines.push('Health score: ' + a.score + (a.scoreProvisional ? ' (preliminary)' : '')); lines.push('Alternates found: ' + a.alternates.length); lines.push(''); if(a.alternates.length){ lines.push('Mapping:'); a.alternates.forEach(function(alt, i){ lines.push(' ' + (i + 1) + '. hreflang="' + (alt.raw.tag || '') + '" href="' + (alt.raw.href || '') + '"'); if(alt.issues && alt.issues.length){ alt.issues.forEach(function(msg){ lines.push(' ! ' + msg); }); } }); lines.push(''); } lines.push('Findings:'); a.checks.forEach(function(c){ var tag = c.severity === 'error' ? 'ERROR' : c.severity === 'warning' ? 'WARN ' : c.severity === 'info' ? 'INFO ' : 'PASS '; lines.push(' [' + tag + '] ' + c.title); if(c.detail) lines.push(' ' + c.detail); if(c.tag) lines.push(' tag: ' + c.tag); if(c.url) lines.push(' url: ' + c.url); if(c.fix) lines.push(' fix: ' + c.fix); }); lines.push(''); }); return lines.join('\n'); } function buildCsv(){ var head = ['Source', 'Page URL', 'Language', 'Destination URL', 'Host', 'Duplicates', 'Self-reference', 'Status', 'Issues', 'Suggested fix']; var lines = [head.map(csvCell).join(',')]; collectAllAlternates().forEach(function(p){ lines.push([ p.a.label || '', p.a.pageUrl || '', p.alt.raw.tag || '', p.alt.raw.href || '', p.alt.host || '', p.alt.dupStatus || '', p.alt.selfStatus || '', (p.alt.issues && p.alt.issues.length) ? 'Error' : 'OK', (p.alt.issues || []).join(' '), p.alt.fix || '' ].map(csvCell).join(',')); }); return '\uFEFF' + lines.join('\r\n') + '\r\n'; } function buildJson(){ return JSON.stringify({ tool: 'EnjoySEOs Hreflang Checker', version: 1, generated: new Date().toISOString(), sources: state.analyses.map(function(a){ return { label: a.label, pageUrl: a.pageUrl, canonical: a.canonical, ok: a.ok, errorMessage: a.errorMessage || '', score: a.score, scoreProvisional: !!a.scoreProvisional, counts: a.counts, alternates: a.alternates.map(function(x){ return { tag: x.raw.tag, href: x.raw.href, normalizedTag: x.tagNorm, normalizedHref: x.hrefNorm, host: x.host, issues: x.issues || [], suggestedFix: x.fix || '' }; }), checks: a.checks.map(function(c){ return { severity: c.severity, title: c.title, detail: c.detail, tag: c.tag, url: c.url, fix: c.fix }; }) }; }) }, null, 2); } /* ------------------------------------------------------------------ * * Export buttons * ------------------------------------------------------------------ */ on($('ehc-copyTags'), 'click', function(){ var txt = buildTagsSnippet(); if(!txt){ showMsg(['There are no hreflang tags to copy yet.']); return; } copyText(txt, this, 'Copy hreflang tags'); }); on($('ehc-copyFix'), 'click', function(){ var txt = buildFixedSnippet(); if(!txt){ showMsg(['There is no hreflang snippet to generate yet.']); return; } copyText(txt, this, 'Copy fixed snippet'); }); on($('ehc-copyReport'), 'click', function(){ if(!state.analyses.length){ showMsg(['Run an analysis first.']); return; } copyText(buildReport(), this, 'Copy report'); }); on($('ehc-expCsv'), 'click', function(){ if(!state.analyses.length){ showMsg(['Run an analysis first.']); return; } download(buildCsv(), 'hreflang-report.csv', 'text/csv;charset=utf-8'); }); on($('ehc-expTxt'), 'click', function(){ if(!state.analyses.length){ showMsg(['Run an analysis first.']); return; } download(buildReport(), 'hreflang-report.txt'); }); on($('ehc-expJson'), 'click', function(){ if(!state.analyses.length){ showMsg(['Run an analysis first.']); return; } download(buildJson(), 'hreflang-report.json', 'application/json;charset=utf-8'); }); /* ------------------------------------------------------------------ * * Initial state * ------------------------------------------------------------------ */ selectMode('url'); clearResults(); })();
Explore more
More free SEO tools

Keep going with other free EnjoySEOs tools. No signup, no limits.

Word CounterCount words, sentences and reading time instantly.Content & Text Character CounterCheck characters with and without spaces.Content & Text Text Comparison ToolSpot differences between two blocks of text.Content & Text SEO Content AnalyzerReview your copy for on-page SEO signals.Content & Text SEO Readability CheckerScore how easy your content is to read.Content & Text Heading CheckerAudit H1–H6 structure and hierarchy.Content & Text Image Alt Text CheckerFind images with missing or weak alt text.Content & Text Keyword Density CheckerSee word and phrase frequency at a glance.Keywords Keyword Clustering ToolGroup related keywords into topic clusters.Keywords Keyword Combination ToolMix keyword lists into every combination.Keywords Schema Markup GeneratorCreate JSON-LD for common schema types.Schema Markup Schema Markup ValidatorValidate structured data before you publish.Schema Markup Local Business Schema GeneratorMarkup for your business name, address and hours.Schema Markup FAQ Schema GeneratorTurn questions and answers into FAQ markup.Schema Markup Product Schema GeneratorProduct, price and offer structured data.Schema Markup Breadcrumb Schema GeneratorBreadcrumb trail markup, ready to paste.Schema Markup Robots.txt GeneratorBuild crawler rules and copy robots.txt.Crawling & Indexing Robots.txt TesterTest whether a URL is allowed or blocked.Crawling & Indexing Noindex CheckerCheck pages for noindex directives.Crawling & Indexing Canonical Tag CheckerVerify the canonical URL on any page.Crawling & Indexing Canonical Tag GeneratorGenerate clean rel=canonical tags.Crawling & Indexing Disavow File GeneratorBuild a disavow file for Google Search Console.Crawling & Indexing XML Sitemap GeneratorCreate a valid sitemap from your URLs.Sitemaps XML Sitemap ValidatorCatch errors in your sitemap file.Sitemaps XML Sitemap VisualizerSee your sitemap structure at a glance.Sitemaps Sitemap URL ExtractorPull every URL out of a sitemap.Sitemaps Meta Description GeneratorWrite meta descriptions that fit the snippet.Meta & Social SEO Title GeneratorDraft title tags with the right length.Meta & Social Meta Tag CheckerInspect the meta tags on any page.Meta & Social Meta Tag GeneratorTitle, canonical, robots and social tags.Meta & Social Google SERP SimulatorPreview your snippet on desktop and mobile.Meta & Social Open Graph PreviewSee Facebook and LinkedIn share cards.Meta & Social Open Graph Tag GeneratorGenerate og: tags for better sharing.Meta & Social Hreflang GeneratorCreate hreflang tags or sitemap entries.International Hreflang CheckerValidate language and region annotations.International Internal Link CheckerReview internal links and anchors on a page.Links & URLs Bulk URL CleanerClean, dedupe and normalise URL lists.Links & URLs HTML Link ExtractorExtract every link from pasted HTML.Links & URLs UTM BuilderAdd campaign tracking parameters to links.Links & URLs URL Parameter BuilderAppend custom query parameters to URLs.Links & URLs Htaccess Redirect GeneratorApache 301 rules, copy-ready.Redirects Nginx Redirect GeneratorNginx redirect blocks in seconds.Redirects

Check Your Hreflang Tags for International SEO

Websites that serve visitors in multiple languages or countries need a reliable way to indicate which version of a page is intended for each audience. Hreflang annotations help search engines understand the relationships between these localized versions. Incorrect codes, missing return links, or inconsistent URLs can make an otherwise useful international SEO setup harder to maintain.

A hreflang checker helps you review your existing implementation and identify potential configuration problems. Use the tool above to examine your hreflang setup, review relevant warnings, and determine which issues need attention before you publish or update localized pages.

What Is a Hreflang Checker?

A hreflang checker is an SEO tool used to inspect the language and regional annotations associated with a web page. These annotations connect alternative versions of substantially equivalent content, such as an English page for the United States, an English page for the United Kingdom, and a German translation.

Hreflang attributes tell search engines about the intended language or regional audience for each URL. A checker can help you examine whether the annotations use recognized codes, contain complete URLs, reference the correct alternatives, and satisfy the other technical requirements supported by the checker.

This is especially helpful for international businesses, multilingual publishers, ecommerce stores, software companies, and organizations with country-specific websites. When a site has many localized pages, checking the relationships manually can become difficult.

Validate Language Codes

Review language and regional identifiers to catch common formatting mistakes, unsupported values, or country codes used without a language identifier.

Review Alternate URLs

Check whether alternate page references use complete URLs with the appropriate protocol instead of relative paths or incomplete addresses.

Identify Missing References

Look for self-referencing annotations and reciprocal links between localized versions so that the relationships are consistent across the relevant pages.

What Is Hreflang in SEO?

Hreflang is an HTML attribute used to identify alternate versions of a page for different languages or regional audiences. It helps search engines understand which localized URL may be appropriate to show to a user based on the page's intended language or region.

For example, an online store might offer the same product category in English for the United States and the United Kingdom. The two pages may have similar descriptions but show different currencies, spelling conventions, delivery information, or local offers. Hreflang annotations help communicate that these URLs are related regional versions.

Hreflang is also useful when a website publishes translations of the same article, service page, or product page. It does not translate content, force a browser redirect, or guarantee that a particular URL will appear in search results. Search engines still determine which pages to crawl, index, and display.

How to Use the Hreflang Checker

Start by identifying a page that has one or more alternate language or regional versions. Use the input method provided by the checker, then review the detected annotations and any reported issues.

  1. Choose the Page to Check

    Identify the URL or page source you want to review. Select a page that should have alternate versions so you can examine the relationships between the localized URLs.

  2. Run the Hreflang Analysis

    Provide the page URL, source code, or other supported input to the checker. The information it can validate depends on the implementation method and input format supported by the tool.

  3. Review the Detected Annotations

    Examine the language codes, alternate URLs, self-references, and other available checks. Pay attention to missing entries, duplicate language mappings, and potentially incorrect URLs.

  4. Fix Issues and Verify Again

    Correct the relevant page templates, tags, or sitemap entries. Recheck the updated implementation and review the corresponding localized pages to confirm that their references agree.

Common Hreflang Errors to Check

Hreflang problems often arise when a website adds a new market, changes URL structures, updates a multilingual template, or publishes translations independently. The following errors are worth checking during an international SEO audit.

Missing Self-Reference

Each localized version should include a hreflang annotation for its own URL as well as for the other versions in its alternate set.

Missing Return Links

If page A references page B as an alternate, page B should also reference page A. Missing reciprocal annotations can prevent search engines from using the intended relationship.

Invalid Language or Region Codes

Mistakes such as using a country code alone or using the wrong regional abbreviation can make an annotation invalid or ineffective.

Relative or Incorrect URLs

Alternate page URLs should be fully qualified and include the protocol, such as HTTPS. Check for old paths, incorrect domains, and URLs that point to the wrong locale.

Duplicate Language Mappings

Review cases where the same language or regional code points to multiple URLs in the same alternate set. Every language or locale mapping should be intentional and unambiguous.

Inconsistent Canonical URLs

Check whether canonical tags and hreflang annotations agree about the intended versions of a page. A localized page canonicalized to a different locale may conflict with the intended indexing setup.

Check the Whole Alternate Set

A successful check on one page does not automatically prove that every localized page is configured correctly. Review the related URLs too, because missing return links and inconsistent mappings may exist only on particular language or regional versions.

Hreflang Language and Region Codes

A hreflang value normally contains a language code and may also contain a region code. The language identifies the language of the version, while the region identifies the intended geographic audience. The two components are separated by a hyphen.

Hreflang Value Intended Use
en English content without a specified region.
en-US English content intended for the United States.
en-GB English content intended for the United Kingdom.
en-AU English content intended for Australia.
fr-CA French content intended for Canada.
de German content without a specified region.
es Spanish content without a specified region.
x-default A fallback URL for audiences not covered by the other annotations.

Language codes generally follow the ISO 639-1 standard, and region codes generally follow ISO 3166-1 alpha-2. The language component comes first. For example, en-GB represents English for the United Kingdom, while fr-CA represents French for Canada.

A common mistake is using en-UK when the recognized region code for the United Kingdom is GB. Another is specifying a country code such as US without a language. Format regional codes according to the relevant standard, and verify unfamiliar combinations before publishing.

What Does x-default Mean?

The x-default value identifies a fallback page for users whose language or region does not match a specified alternate. It is often used for a language or country selector, or another appropriate default destination. It is recommended for relevant setups but is not required on every website.

Example of Correct Hreflang Tags

Consider a website that publishes a page for general English readers, a separate version for the United States, and a translation for German readers. An HTML implementation can place alternate annotations in the head section of the page.

<link rel="alternate"
      hreflang="en"
      href="https://example.com/en/page/" />

<link rel="alternate"
      hreflang="en-US"
      href="https://example.com/us/page/" />

<link rel="alternate"
      hreflang="de"
      href="https://example.com/de/page/" />

<link rel="alternate"
      hreflang="x-default"
      href="https://example.com/language-selector/" />

These URLs are illustrative examples. A production implementation must use the actual URLs of your equivalent localized pages. The same alternate set should be included on each applicable version, including the German and US pages, so that the references are reciprocal.

Why the Self-Referencing Tag Matters

The page at the English URL should include an annotation pointing back to that English URL. The US and German versions should likewise reference themselves and their corresponding alternates. This makes it clear that all the URLs are members of the same localized content set.

Where Can Hreflang Be Implemented?

Hreflang annotations can be implemented in three main ways: HTML link elements, HTTP response headers, and XML sitemaps. Google treats these methods as equivalent for identifying alternate versions. Most websites can select the method that best fits their publishing system.

HTML Head Tags

Add link elements with rel="alternate" and hreflang attributes inside the page's head section. This is a common approach for standard website pages.

HTTP Headers

Return alternate-language information in HTTP Link headers. This can be useful for files that do not contain ordinary HTML, such as PDFs.

XML Sitemaps

Add XHTML link entries to sitemap records to describe the localized versions of each URL. This can help with large sites that manage many alternate pages.

You do not need to implement all three methods for every page. Choose an approach that your site can maintain consistently. Also check that your validation method can actually read the implementation you use. A checker that inspects only HTML source may not detect annotations that exist exclusively in HTTP headers or a sitemap.

Hreflang, Canonical Tags, and Indexing

Hreflang and canonical tags have different purposes. Hreflang describes the relationship between localized versions of content. A canonical annotation indicates which URL should be treated as the preferred version among duplicate or substantially similar pages.

For distinct localized URLs that should be available in search, each page would commonly declare itself as canonical and include the other relevant localized URLs in its hreflang set. This helps keep the intended canonical URL for each locale consistent with the alternate-language relationships.

For example, the French version of a page should not accidentally canonicalize to an unrelated English URL when the intention is for both language versions to be independently available in search. Review canonical tags alongside hreflang rather than assuming that alternate annotations override canonicalization.

A valid hreflang annotation also does not guarantee that a page is indexed. Check whether the URL is accessible to crawlers, whether it returns the intended response, and whether any robots or noindex directives prevent the page from being indexed as intended.

Hreflang Best Practices for International SEO

A reliable hreflang implementation depends on consistency across all localized pages. Use the following practices when launching a new language, updating regional URLs, or auditing an international website.

  • Use the correct language and region codes. Verify each language identifier and any optional regional identifier instead of guessing based on familiar abbreviations.
  • Use fully qualified URLs. Include the protocol and complete URL for every alternate page, rather than relying on relative links.
  • Include the current page in its alternate set. Every localized version should reference itself as well as the other relevant URLs.
  • Keep the alternate references reciprocal. Review all related language versions to ensure they describe the same set of localized alternatives.
  • Use x-default where appropriate. Consider a fallback destination for users whose language or region is not explicitly covered by your site.
  • Check canonical consistency. Ensure that the canonical strategy agrees with the localized URL structure and the pages you intend search engines to index.
  • Update annotations when URLs change. When pages move or a market launches, update the corresponding tags and sitemap entries rather than leaving references to old URLs.
  • Use translations that serve the intended audience. Hreflang labels relationships between content versions; it does not replace accurate translations or useful regional content.
  • Audit the whole localized page group. Check the other language and regional versions, not just the page from which the audit started.
  • Recheck after technical changes. Test the implementation after redesigns, plugin updates, URL migrations, or changes to multilingual templates.

Who Should Use a Hreflang Checker?

Hreflang validation is particularly valuable for websites that operate across language or regional boundaries. The larger the alternate URL structure becomes, the more difficult it can be to maintain all references manually.

International Ecommerce Stores

Review country-specific product and category pages where pricing, shipping information, currency, or language differs between target markets.

Multilingual Websites

Check that translated versions of articles, landing pages, and service pages reference the correct alternatives.

International SEO Teams

Review implementations after migrations, template updates, new market launches, or changes to a site's language structure.

Developers and Website Owners

Validate generated annotations, troubleshoot incorrect links, and confirm that language mappings remain consistent across a multilingual publishing system.

SEO Consultants and Auditors

Examine hreflang configurations as part of a technical SEO audit alongside canonical tags, indexability, redirects, and sitemap health.

SaaS and Global Businesses

Review localized product documentation, support pages, feature descriptions, and other content built for different language or regional audiences.

Frequently Asked Questions

Find answers to common questions about hreflang tags, international SEO, alternate URLs, and hreflang validation.

What is a hreflang checker?

A hreflang checker is an SEO tool that examines hreflang annotations used to identify alternate language or regional versions of a page. Depending on its capabilities, it can help detect incorrect codes, missing references, and other implementation problems.

What does hreflang do in SEO?

Hreflang tells search engines about alternate versions of a page for different languages or regions. It helps search engines understand which localized version may be appropriate for a particular audience.

How do I check hreflang tags?

Use a checker that supports your implementation method. Inspect the detected language codes and alternate URLs, verify that each page references itself, and check that related pages link back to one another.

Are hreflang tags a Google ranking factor?

Hreflang is primarily a signal for understanding localized versions of content and serving the appropriate version in search. It is not a substitute for useful content, crawlability, indexability, or other SEO considerations.

What is a hreflang return link?

A return link is a reciprocal hreflang reference from one localized page back to another page that references it. For example, if an English page lists a French alternative, the French page should also list the English version in its alternate set.

Is the x-default hreflang tag required?

No. The x-default value is used to identify a fallback destination for users who do not match the specified language or region alternatives. It is recommended for suitable implementations but is not mandatory for every page.

Can I use hreflang across different domains?

Yes. Alternate URLs do not need to share the same domain. For example, a business may use different country-specific domains for its localized pages, provided that the annotations correctly identify the related versions.

What is the difference between hreflang and canonical tags?

Hreflang identifies alternate language or regional versions of content. Canonical tags indicate a preferred URL among duplicate or substantially similar pages. They have different purposes and should be configured consistently.

Can hreflang tags be added to an XML sitemap?

Yes. XML sitemaps can include XHTML link entries that list alternate language or regional versions for each URL. Each localized URL should include the complete set of relevant alternate entries, including itself.

Can hreflang be implemented through HTTP headers?

Yes. HTTP Link response headers can communicate alternate language versions. This can be particularly useful for non-HTML resources, such as PDF files.

Does hreflang automatically translate a page?

No. Hreflang only identifies relationships between alternate versions. You must publish and maintain the appropriate translated or regionally adapted content separately.

Why are hreflang annotations not being recognized?

Possible causes include invalid codes, missing return links, incorrect alternate URLs, inconsistent implementation, or canonical and indexing conflicts. Check the affected pages and their alternate sets to isolate the problem.

Should every page on a website use hreflang?

No. Hreflang is useful for pages that have equivalent language or regional alternatives. A page with no relevant localized version does not need an alternate set merely because the rest of the website supports multiple languages.

Does a valid hreflang setup guarantee indexing?

No. Hreflang helps describe alternate page relationships, but it does not guarantee crawling, indexing, rankings, or display of a particular URL. Each page still needs to satisfy applicable technical and content requirements.