Hreflang — International SEO
The hreflangs field lets editors configure language and region alternates directly in Sanity Studio using BCP 47 tags (e.g. en, fr-FR, x-default). The entries are stored on the document and queried via GROQ so your frontend can render the correct <link rel="alternate"> tags.
About hreflang
Google uses hreflang annotations to serve the correct language/region version of a page to the right audience. Each entry maps a BCP 47 locale tag (en-US, de, etc.) to the absolute URL of the alternate page. Always include an x-default entry for the catch-all / language selector URL.
hreflangEntry Schema
Each item in the hreflangs array is an hreflangEntry object with two validated fields:
| Field | Type | Required | Description |
|---|---|---|---|
locale | string | Yes | BCP 47 tag — e.g. en, fr-FR, x-default. Validated with a regex pattern in Studio. |
url | url | Yes | The absolute URL of the alternate page (must be http or https). |
Plugin Setup
The hreflangs field is part of the seoFields schema and is registered automatically. No extra configuration is needed:
import seofields from 'sanity-plugin-seofields'
import { defineConfig } from 'sanity'
export default defineConfig({
plugins: [
seofields(), // hreflangs array is included automatically
],
})Using with Field Groups
Place hreflangs in its own field group tab for cleaner editing:
seofields({
fieldGroups: [
{
name: 'meta',
title: 'Meta',
default: true,
fields: ['title', 'description', 'metaImage', 'keywords', 'canonicalUrl', 'robots'],
},
{ name: 'openGraph', title: 'Open Graph', fields: ['openGraph'] },
{ name: 'twitter', title: 'Twitter Card', fields: ['twitter'] },
{
name: 'international',
title: 'International',
fields: ['hreflangs'], // ← dedicated tab
},
],
})Hiding the field
Hide hreflangs globally or per document type if you don't need international SEO:
seofields({
// Hide globally
defaultHiddenFields: ['hreflangs'],
// — or — hide per document type
fieldVisibility: {
internalPage: { hiddenFields: ['hreflangs'] },
},
})GROQ Query
*[_type == "page" && slug.current == $slug][0]{
title,
seo {
title,
description,
hreflangs[] {
locale,
url
}
}
}Rendering in Next.js
Pass the queried hreflang entries to Next.js generateMetadata() via the alternates property:
import type { Metadata } from 'next'
import { buildSeoMeta } from 'sanity-plugin-seofields/next'
import { sanityFetch } from '@/sanity/lib/live'
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const { data } = await sanityFetch({ query: PAGE_QUERY, params: { slug } })
const base = buildSeoMeta({ seo: data?.seo, baseUrl: 'https://example.com', path: `/${slug}` })
// Build hreflang alternates from Sanity data
const languages = data?.seo?.hreflangs?.reduce(
(acc: Record<string, string>, entry: { locale: string; url: string }) => {
acc[entry.locale] = entry.url
return acc
},
{} as Record<string, string>,
)
return {
...base,
alternates: {
...base.alternates,
languages,
},
}
}Manual <link> tags
For non-Next.js frameworks or the Pages Router, render the <link rel="alternate"> tags manually:
export default function Layout({ page }) {
return (
<html>
<head>
{page.seo?.hreflangs?.map(({ locale, url }) => (
<link key={locale} rel="alternate" hrefLang={locale} href={url} />
))}
</head>
<body>...</body>
</html>
)
}Best practice: Always add an x-default entry pointing to your language selector or default language URL. Every page in your hreflang set must also include a reciprocal annotation pointing back — Google ignores one-sided annotations.
TypeScript
import type { SeoFields } from 'sanity-plugin-seofields'
// hreflangs is typed on SeoFields
const entries: Array<{ locale: string; url: string }> | undefined =
document.seo?.hreflangsWas this page helpful?