If your app serves multiple languages or regions, search engines need to know which version of each page to show to which audience. That is what hreflang does. Without it, Google may show the English version of your product page to a French user, or the US-priced listing to someone in Germany.
For apps with deep links, hreflang adds another layer: you need to coordinate the web page language variants with the app deep link targets. This guide covers how to do that correctly. For the general app indexing strategy, see app indexing and SEO for mobile apps. For canonical URL strategy across app content, see canonical URLs for app content.
How Hreflang Works
The Basics
Hreflang tells search engines: "This page exists in multiple language/region versions. Here are the URLs for each version." Google then serves the correct version based on the searcher's language and location.
<head>
<link rel="alternate" hreflang="en" href="https://www.yourapp.com/products/shoes" />
<link rel="alternate" hreflang="es" href="https://www.yourapp.com/es/products/shoes" />
<link rel="alternate" hreflang="fr" href="https://www.yourapp.com/fr/products/shoes" />
<link rel="alternate" hreflang="de" href="https://www.yourapp.com/de/products/shoes" />
<link rel="alternate" hreflang="x-default" href="https://www.yourapp.com/products/shoes" />
</head>
The x-default value specifies the fallback version for users whose language/region does not match any listed variant.
Language vs. Region
Hreflang supports both language-only and language-region codes:
| Code | Meaning |
|---|---|
en |
English (any region) |
en-US |
English for United States |
en-GB |
English for United Kingdom |
es |
Spanish (any region) |
es-MX |
Spanish for Mexico |
pt-BR |
Portuguese for Brazil |
Use language-region codes when the content differs by region (pricing, availability, legal terms). Use language-only codes when all regions share the same translation.
URL Structures for Multilingual Deep Links
Option 1: Subdirectory per Language
https://www.yourapp.com/products/shoes (English, default)
https://www.yourapp.com/es/products/shoes (Spanish)
https://www.yourapp.com/fr/products/shoes (French)
https://www.yourapp.com/de/products/shoes (German)
This is the simplest approach. All content lives on one domain, preserving link equity. The deep link path includes the language prefix.
App deep link handling:
// iOS: Extract language from URL path
func handleDeepLink(_ url: URL) {
let pathComponents = url.pathComponents
let supportedLanguages = ["es", "fr", "de", "ja", "ko"]
var language = "en"
var contentPath = url.path
if pathComponents.count > 1, supportedLanguages.contains(pathComponents[1]) {
language = pathComponents[1]
contentPath = "/" + pathComponents.dropFirst(2).joined(separator: "/")
}
setAppLanguage(language)
navigateToContent(contentPath)
}
Option 2: Separate Domains per Region
https://www.yourapp.com/products/shoes (US)
https://www.yourapp.co.uk/products/shoes (UK)
https://www.yourapp.de/products/shoes (Germany)
https://www.yourapp.com.br/products/shoes (Brazil)
Each domain needs its own App Links and Universal Links verification files. This multiplies the configuration work.
Option 3: Subdomain per Language
https://en.yourapp.com/products/shoes
https://es.yourapp.com/products/shoes
https://fr.yourapp.com/products/shoes
Similar to separate domains in terms of verification complexity. Each subdomain needs its own assetlinks.json and apple-app-site-association files.
Recommendation: Use subdirectories unless you have strong business reasons for separate domains (different legal entities, separate app builds per region).
Hreflang Implementation for Deep Link Pages
HTML Link Tags
Add hreflang tags to every language variant of each page:
<!-- On https://www.yourapp.com/products/shoes -->
<head>
<link rel="alternate" hreflang="en" href="https://www.yourapp.com/products/shoes" />
<link rel="alternate" hreflang="es" href="https://www.yourapp.com/es/products/shoes" />
<link rel="alternate" hreflang="fr" href="https://www.yourapp.com/fr/products/shoes" />
<link rel="alternate" hreflang="x-default" href="https://www.yourapp.com/products/shoes" />
<link rel="canonical" href="https://www.yourapp.com/products/shoes" />
</head>
<!-- On https://www.yourapp.com/es/products/shoes -->
<head>
<link rel="alternate" hreflang="en" href="https://www.yourapp.com/products/shoes" />
<link rel="alternate" hreflang="es" href="https://www.yourapp.com/es/products/shoes" />
<link rel="alternate" hreflang="fr" href="https://www.yourapp.com/fr/products/shoes" />
<link rel="alternate" hreflang="x-default" href="https://www.yourapp.com/products/shoes" />
<link rel="canonical" href="https://www.yourapp.com/es/products/shoes" />
</head>
Every page must reference all other language variants, including itself. This bidirectional confirmation is how Google validates the hreflang setup.
Sitemap Implementation
For large apps with thousands of pages, implementing hreflang in sitemaps is more maintainable than HTML tags:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://www.yourapp.com/products/shoes</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://www.yourapp.com/products/shoes" />
<xhtml:link rel="alternate" hreflang="es" href="https://www.yourapp.com/es/products/shoes" />
<xhtml:link rel="alternate" hreflang="fr" href="https://www.yourapp.com/fr/products/shoes" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.yourapp.com/products/shoes" />
</url>
<url>
<loc>https://www.yourapp.com/es/products/shoes</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://www.yourapp.com/products/shoes" />
<xhtml:link rel="alternate" hreflang="es" href="https://www.yourapp.com/es/products/shoes" />
<xhtml:link rel="alternate" hreflang="fr" href="https://www.yourapp.com/fr/products/shoes" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://www.yourapp.com/products/shoes" />
</url>
</urlset>
Deep Link Routing by Language
Server-Side Language Detection
When a deep link URL is accessed, detect the user's language and route accordingly:
app.get('/products/:slug', (req, res) => {
const acceptLanguage = req.headers['accept-language'];
const userLang = parseLanguage(acceptLanguage); // e.g., 'es'
const localizedPath = userLang !== 'en' ? `/${userLang}/products/${req.params.slug}` : req.path;
// For app users: deep link opens the localized content
// For web users: redirect to the localized page
if (isBot(req.headers['user-agent'])) {
// Serve the English version to bots (they follow hreflang for other versions)
renderPage(req.params.slug, 'en', res);
} else {
res.redirect(302, localizedPath);
}
});
App-Side Language Handling
When the app opens a deep link, use the URL's language prefix to set the content language:
// Android: handle localized deep links
class ProductActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data ?: return
val pathSegments = uri.pathSegments
// Check if first segment is a language code
val supportedLangs = setOf("es", "fr", "de", "ja", "ko", "pt")
val lang = if (pathSegments.isNotEmpty() && supportedLangs.contains(pathSegments[0])) {
pathSegments[0]
} else {
"en"
}
val slug = pathSegments.last()
viewModel.loadProduct(slug, lang)
}
}
Structured Data for Multilingual Content
Localized Structured Data
Each language variant should have its own structured data with localized values:
<!-- English version -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Running Shoes",
"description": "Lightweight running shoes for daily training",
"offers": {
"@type": "Offer",
"price": "120.00",
"priceCurrency": "USD"
},
"potentialAction": {
"@type": "ViewAction",
"target": "https://www.yourapp.com/products/running-shoes"
}
}
</script>
<!-- Spanish version -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Zapatillas de Running",
"description": "Zapatillas ligeras para entrenamiento diario",
"offers": {
"@type": "Offer",
"price": "110.00",
"priceCurrency": "EUR"
},
"potentialAction": {
"@type": "ViewAction",
"target": "https://www.yourapp.com/es/products/running-shoes"
}
}
</script>
Common Mistakes
Missing Return Links
Every hreflang annotation must be bidirectional. If page A says "my Spanish version is page B," page B must say "my English version is page A." If either side is missing, Google ignores the entire hreflang relationship for that pair.
Conflicting Canonical and Hreflang
Do not set the canonical URL of a localized page to the default language version. Each language variant should have its own self-referencing canonical:
<!-- Wrong: Spanish page canonicalized to English -->
<link rel="canonical" href="https://www.yourapp.com/products/shoes" />
<!-- Correct: Spanish page canonicalized to itself -->
<link rel="canonical" href="https://www.yourapp.com/es/products/shoes" />
Missing x-default
Always include x-default to handle users whose language/region does not match any variant. Without it, Google guesses which version to show.
Tolinku and International Deep Links
Tolinku supports deep link routing with path-based rules. Configure routes like /:lang/products/:slug in the Tolinku dashboard to handle localized deep links. The route parameters pass the language code to the app, which loads the content in the correct language.
For banner localization on multilingual sites, see localizing smart banners. For the broader app indexing strategy, see app indexing and SEO for mobile apps.
Get deep linking tips in your inbox
One email per week. No spam.