seo-hreflang
Hreflang and internationalization SEO rules. Use when working with locale routing, language switching, alternate language links, the /{lang}-{country}/ URL pattern, i18n configuration, or middleware locale detection.
Hreflang & Internationalization SEO Guidelines
Technical rules for implementing locale routing and multi-language hreflang tags in a Next.js e-commerce site. These patterns apply to any multi-locale site — adapt the locale list to match your project's configuration.
For locale configuration patterns, see locale-matrix.md.
Hreflang Link Tags
Every indexable page MUST include <link rel="alternate" hreflang="xx-YY"> tags for ALL locale variants of that page, plus an x-default entry.
Rules
- All variants must be listed — if a page exists in N locales, there must be N+1 link tags (all locales + x-default)
- Self-referencing required — the current page's locale must be included in its own hreflang tags
- x-default — should point to the default/international locale as the fallback for unmatched users
- URLs must be absolute — full URL with protocol, domain, and locale prefix
- Bidirectional — if page A hreflang-links to page B, page B must hreflang-link back to page A
- Same content — hreflang links must connect equivalent pages (same product, same category), not different pages
- Hreflang value format — use lowercase language, uppercase country:
en-GB,de-DE,fr-FR(ISO 639-1 + ISO 3166-1 Alpha-2)
Implementation in Next.js
App Router: Use the alternates.languages field in generateMetadata. Pages Router: Render <link rel="alternate"> tags via next/head.
// Example imports — use your project's actual config
import { SITE_URL } from '@/config/site';
import { ALL_LOCALES, DEFAULT_LOCALE } from '@/config/locales';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, id, slug } = params;
const path = `/products/${id}/${slug}`; // adapt to your project's URL structure
// Build hreflang map dynamically from locale config
const languages: Record<string, string> = {
'x-default': `${SITE_URL}/${DEFAULT_LOCALE}${path}`,
};
for (const loc of ALL_LOCALES) {
languages[loc] = `${SITE_URL}/${loc}${path}`;
}
return {
alternates: {
canonical: `${SITE_URL}/${locale}${path}`,
languages,
},
};
}
For the root layout (applies to all pages). In Pages Router, place this logic in _app.tsx or a shared layout component.
// Layout file (e.g. app/[locale]/layout.tsx or a shared layout component)
// Example imports — use your project's actual config
import { SITE_URL } from '@/config/site';
import { ALL_LOCALES, DEFAULT_LOCALE } from '@/config/locales';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const languages: Record<string, string> = {
'x-default': `${SITE_URL}/${DEFAULT_LOCALE}/`,
};
for (const loc of ALL_LOCALES) {
languages[loc] = `${SITE_URL}/${loc}/`;
}
return {
alternates: {
languages,
},
};
}
Hreflang in Sitemaps (best practice)
In addition to HTML link tags, include hreflang in XML sitemaps using <xhtml:link>:
<url>
<loc>https://example.com/en-GB/products/123/blue-widget-pro</loc>
<xhtml:link rel="alternate" hreflang="en-GB" href="https://example.com/en-GB/products/123/blue-widget-pro"/>
<xhtml:link rel="alternate" hreflang="de-DE" href="https://example.com/de-DE/products/123/blue-widget-pro"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en-US/products/123/blue-widget-pro"/>
<!-- ... all locales -->
</url>
HTTP Header Method
For non-HTML resources (PDFs, downloadable files), use HTTP Link headers:
Link: <https://example.com/en-US/file.pdf>; rel="alternate"; hreflang="en-US",
<https://example.com/de-DE/file.pdf>; rel="alternate"; hreflang="de-DE",
<https://example.com/en-US/file.pdf>; rel="alternate"; hreflang="x-default"
Google treats all three methods (HTML tags, HTTP headers, sitemaps) as equivalent signals. There is no benefit from combining multiple methods for the same page — pick whichever is most convenient.
Multi-Language Countries
Several countries have multiple language variants. All variants must cross-reference each other:
| Country | Locales |
|---|---|
| Switzerland | en-CH, de-CH, fr-CH, it-CH |
| Belgium | en-BE, nl-BE, fr-BE, de-BE |
| Canada | en-CA, fr-CA |
| Netherlands | en-NL, nl-NL |
All locale variants must be included in hreflang — do not only link within the same country. A German-speaking user in Switzerland (de-CH) should also see links to de-DE and de-BE.
Language Switcher Component
The language/country switcher in the UI must:
- Link to the same page in the target locale — not just the homepage of that locale
- Use
<Link>from next/link — enables client-side navigation - Preserve URL path — switching from
/en-GB/products/123/blue-widget-proto German should go to/de-DE/products/123/blue-widget-pro - Be visible and crawlable — search engines use the switcher to discover locale variants
Middleware Locale Detection
// middleware.ts
// Example imports — use your project's actual config
import { ALL_LOCALES, DEFAULT_LOCALE } from '@/config/locales';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Check if pathname already has a locale prefix
const pathnameHasLocale = ALL_LOCALES.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
// Detect preferred locale from Accept-Language header
const locale = detectLocale(request) || DEFAULT_LOCALE;
// 302 redirect (temporary) — locale preference may change
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
}
Use 302 (temporary) for locale detection redirects — the user's preferred locale may change, so this should not be a permanent redirect.
Common Technical Anti-Patterns
- Missing self-referencing hreflang — the page must include itself in the hreflang set
- Missing x-default — always include x-default pointing to the default locale
- Language switcher links to locale homepage — must preserve the current page path
- One-directional hreflang — if en-GB links to de-DE, de-DE must link back to en-GB
- Hardcoded locale list — always derive from a single config source (e.g. a shared locale config array)
- 301 for locale detection — use 302 (temporary) for Accept-Language redirects
- Missing locales from hreflang — every locale the page is available in must be listed, or Google may not associate them
Why it matters: Incorrect hreflang causes search engines to show the wrong language version to users, or to treat locale variants as duplicate content. For a multi-locale site, this can massively dilute ranking signals.
Important caveat: Google treats hreflang as a hint, not a directive. Google may still choose a different locale version based on canonical tags, site structure, or content similarity. Correct implementation improves the odds of Google serving the right version, but does not guarantee it.