Sanity multilingual site losing traffic: hreflang and translation fixes
By Nayan Kyada · · 6 min read
Part of The Sanity + Next.js Guide
If your multilingual Sanity site is getting the wrong language in search results, or your translated pages barely rank at all, the problem is rarely the content. It is almost always the plumbing between Sanity's translation data and the hreflang and canonical tags that Next.js renders. This post walks through the symptoms, what they cost, and the order I fix them in.
Symptoms of a broken multilingual setup
These are the patterns I see most on inherited builds:
- Search Console shows Spanish or German pages as "Duplicate, Google chose different canonical than user".
- UK visitors land on the US page, or French visitors land on English, even though a French page exists.
- The translated pages are indexed but get almost no impressions, while the default language page ranks fine.
- The International Targeting or hreflang-related reports show "no return tags" or alternates pointing at 404s.
- Editors say "we translated that page" but the language switcher links to the homepage instead.
Any one of these means Google cannot reliably map your language versions together. Together they mean you are paying for translation work that search engines are not crediting.
What it costs to ignore
Translation is expensive per page, and the traffic it unlocks is often the cheapest in your funnel because competition in non-English results is thinner. If alternates are wrong, Google may treat translated pages as thin duplicates and fold them into the English one. You keep paying translators and editors, and the pages never earn. There is also a quieter cost: users who land on the wrong language bounce, which does your engagement numbers no favours.
Cause 1: orphaned translations in Sanity
The first thing I check is whether every translated document is actually linked to its siblings. With the document internationalization plugin, links live in a translation.metadata document. If editors duplicated a page by hand instead of using the translate action, no metadata exists, and your code has nothing to build alternates from.
Run this in Vision to find orphans:
// Non-default-language pages that no translation.metadata references
*[_type == "page" && language != "en"
&& count(*[_type == "translation.metadata" && references(^._id)]) == 0]{
_id,
language,
title,
"slug": slug.current
}Every result needs to be linked to its source page before anything else is worth fixing. Do this in the Studio with the plugin's translation tooling, or with a scripted migration if there are hundreds. Also look for the inverse: metadata documents whose references point at deleted documents. Those produce alternates that 404, which is worse than no alternates.
Cause 2: hreflang that is not reciprocal or not absolute
Hreflang only works when every page in the set lists every other page, including itself, with absolute URLs. A common broken build emits alternates only on the English page, or emits relative paths, or lists a locale whose translation is still a draft.
Here is the shape I aim for in Next.js 16 with the metadata API:
// app/[locale]/[slug]/page.tsx
import type { Metadata } from "next";
import { client } from "@/sanity/lib/client";
const SITE = "https://example.com";
const alternatesQuery = `
*[_type == "page" && language == $locale && slug.current == $slug][0]{
"alternates": *[_type == "translation.metadata" && references(^._id)][0]
.translations[defined(value->slug.current)]{
"language": _key,
"slug": value->slug.current
}
}
`;
type Params = Promise<{ locale: string; slug: string }>;
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
const { locale, slug } = await params;
const data = await client.fetch<{ alternates: { language: string; slug: string }[] | null }>(
alternatesQuery,
{ locale, slug }
);
const languages: Record<string, string> = {};
for (const alt of data?.alternates ?? []) {
languages[alt.language] = `${SITE}/${alt.language}/${alt.slug}`;
}
if (languages.en) languages["x-default"] = languages.en;
return {
alternates: {
canonical: `${SITE}/${locale}/${slug}`,
languages,
},
};
}Three details matter here. The canonical is self-referencing per locale, never pointing at English. The filter defined(value->slug.current) drops translations that were never published, because a weak reference to an unpublished document resolves to null. And the keys come from the translation's language id, so make sure those ids match the codes you want in hreflang (en, fr, pt-BR, not english or fr_FR).
Cause 3: slug translation without redirects
Translated slugs are good for SEO, but editors change them, and each change breaks the alternates and any inbound links. If you localise slugs, you need a redirect layer. I covered that mechanism in Sanity slug changes causing 404s, and it applies per locale. Check that the redirect rules include the locale prefix, otherwise a French slug change silently redirects to an English path.
Cause 4: locale routing that serves two URLs for one page
If your default language is reachable both at /about and /en/about, you have duplicate content before translation even enters the picture. Pick one scheme. If you use a routing library such as next-intl, the setup in Sanity multilingual content in Next.js with next-intl shows the configuration that keeps one URL per page. Also check that automatic locale detection by Accept-Language header is not redirecting Googlebot, which crawls mostly from the US without a language preference, away from your translated pages.
Cause 5: sitemap that ignores languages
Your sitemap should list every locale URL, and ideally include alternates per entry. If it only lists default-language URLs, Google discovers translations slowly through links alone. Generate the sitemap from the same translation data as the hreflang tags so they cannot drift apart.
The fix order I use
- Audit orphans and dead metadata in Sanity, and link or remove them.
- Fix canonicals so each locale points to itself.
- Emit reciprocal, absolute hreflang plus x-default from Sanity translation data.
- Remove duplicate URL schemes and detection redirects that catch crawlers.
- Rebuild the sitemap with all locales.
- Resubmit in Search Console and watch the indexing report for a few weeks.
Verify by viewing source on three pages in different languages: each should list the same set of alternates, and each URL in that set should return 200 without redirecting. Search Console will take days to weeks to reflect the change, so judge by the source first.
If your content pipeline is also publishing stale pages, the symptoms can overlap with Next.js pages not indexed by Google. If you are mid-redesign, the redesign without losing SEO checklist covers what to protect.
What to ask a developer
- Can you show me the hreflang output for one page in every language?
- How do you handle a translation that exists as a draft only?
- What happens to alternates and redirects when an editor changes a translated slug?
- How will we know the fix worked, and in what timeframe?
A vague answer to the second or third question usually means the same bug will return.
This is work I do. A Performance & Sanity audit starts from $800 and covers translation data, hreflang and canonicals; details are on the services page.
Frequently asked questions
01Why is Google showing the wrong language version of my Sanity site?
Usually the hreflang tags are missing, point to URLs that 404 or redirect, or are not reciprocal. A canonical that points every locale at the default language page causes the same symptom.
02Do I need hreflang if each language has its own URL?
Separate URLs are required, but hreflang is what tells Google which page serves which language or region. Without it, Google guesses, and often guesses wrong for near-identical English variants.
03How do I find Sanity documents that have no translation linked?
With the document internationalization plugin, query for documents in a non-default language that no translation.metadata document references. Any result is an orphan that cannot produce a correct alternate link.