Skip to content
Tolinku
Tolinku
Sign In Start Free
Marketing · · 7 min read

Hreflang for App Deep Links: International SEO

By Tolinku Staff
|
Tolinku seo app indexing dashboard screenshot for marketing blog posts

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.

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).

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>

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

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 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.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.