Skip to Content

Two languages served from the same URL

What Is This Issue

Separate URLs per Language (Subdirectory or ccTLD, Not Just Cookies)

This issue checks whether your website uses distinct, crawlable URLs for each language or regional version instead of relying solely on cookies, JavaScript redirects, or session-based language switching that hide content variations behind a single URL.

What This Issue Checks

The audit verifies that each language or regional version of a page is accessible at a distinct URL using:

  • Subdirectories (/en/, /fr/)
  • Subdomains (en.example.com, fr.example.com)
  • Country-code top-level domains (ccTLDs like .fr, .de)

And does NOT rely solely on:

  • Cookies to switch languages
  • JavaScript redirects based on browser language
  • Session-based language detection
  • Query string parameters only (e.g., ?lang=fr)

What is Considered a Passing Implementation

Your website passes this check when:

  • Each language version has its own distinct, crawlable URL
  • Hreflang alternate URLs point to distinct URL paths or domains
  • Language switching doesn’t rely solely on cookies or JavaScript
  • The URL structure clearly indicates the language/region (subdirectory, subdomain, or ccTLD)

Real-World Example

Passing Implementation (Subdirectory approach):

https://example.com/en/about/ ← English version https://example.com/fr/about/ ← French version https://example.com/es/about/ ← Spanish version

Failing Implementation (Cookie-based switching):

https://example.com/about/ ← Same URL for all languages Cookie: lang=fr ← Language controlled by cookie

Why Is This Important

Impact on SEO

Crawlability

Google’s crawler is a server-side bot that fetches raw HTTP responses. It does not execute JavaScript, set cookies, or maintain session state between requests. If your website uses cookies or client-side JavaScript to serve different language versions at the same URL, Google can only ever index one version — typically the default.

Indexability

When all language versions are served at the same URL, search engines cannot differentiate between them. They index only the default language version, and all other language variants become invisible to search engines.

Rankings

Users in different regions searching in their own language won’t find your content if it’s not indexed under distinct URLs. This severely limits your international SEO reach and rankings in target markets.

User Experience

While not directly an SEO factor, proper URL structure for languages ensures users can:

  • Bookmark specific language versions
  • Share links to specific language versions
  • Navigate directly to their preferred language version

Duplicate Content

Serving all languages from the same URL doesn’t create duplicate content issues, but it does create “invisible content” issues — search engines only see one language version, making all other language content effectively invisible.

AI Search / AEO

As AI-powered search becomes more prevalent, AI systems need to understand which language version to reference when answering queries. Distinct URLs for each language help AI systems correctly identify and reference the appropriate language version.

How Resolving This Issue Improves SEO Health Score

Fixing separate URLs per language issues improves your overall SEO health score by:

  • Ensuring all language versions are crawlable and indexable
  • Allowing search engines to serve the correct language version to users in different regions
  • Improving international SEO reach and rankings
  • Strengthening hreflang effectiveness (hreflang requires distinct URLs to work properly)
  • Making your multilingual content fully accessible to search engines

How XeoPix Detects This

XeoPix checks whether your website uses distinct, crawlable URLs for each language or regional version, rather than relying solely on cookies, JavaScript redirects, or session-based language switching.

Detection Process

XeoPix follows these logical steps to identify language URL structure issues:

1. Fetch the Page Without Language Cookies

XeoPix issues an HTTP GET request to the target URL without any language cookies set. This simulates how Google’s crawler fetches pages.

2. Extract Hreflang Alternate URLs

XeoPix parses the <head> section to find all <link rel="alternate" hreflang="..."> tags and extracts their href values to analyze the URL structure.

3. Analyze URL Structure

XeoPix examines the hreflang URLs to determine the language URL structure:

  • Subdirectory: URLs contain language path segments (/en/, /fr/)
  • Subdomain: URLs use language subdomains (en.example.com)
  • ccTLD: URLs use different country-code domains (.fr, .de)
  • Query parameter: URLs use language query parameters (?lang=fr)
  • Cookie-only: No distinct URLs; language controlled by cookies

XeoPix inspects:

  • The Set-Cookie HTTP response header for language-related cookies
  • Raw HTML for JavaScript that sets language cookies
  • Raw HTML for cookie-based language switching code

5. Check for JavaScript-Based Language Detection

XeoPix scans the raw HTML for JavaScript code that:

  • Detects navigator.language or navigator.userLanguage
  • Performs redirects based on browser language
  • Sets language preferences without distinct URLs

6. Validate Query String Parameters

XeoPix checks if language is switched via query string parameters (e.g., ?lang=fr, ?locale=en) as the primary mechanism.

When the Issue is Flagged

The issue is flagged when any of these conditions are met:

  • Cookie-only language switching: Language switching appears to rely on cookies rather than distinct URLs
  • Query parameter language switching: Language versions are differentiated only by query string parameters (not recommended by Google)
  • No distinct language URLs detected: No distinct URLs were detected for language variants (Google requires each language version to have a unique, crawlable URL)

When the Issue Passes

The issue passes when:

  • Hreflang alternate URLs use distinct URL paths or domains
  • Language variants are distinguished by URL path segment, subdomain, or domain — not by cookie alone
  • The page does not rely solely on cookies or JavaScript for language switching
  • Each language version has its own indexable URL

How To Fix

Practical Recommendations

Implement one of these URL structures for multilingual content:

Subdirectory (Recommended for most sites):

https://example.com/en/about/ https://example.com/fr/about/ https://example.com/es/about/

Subdomain:

https://en.example.com/about/ https://fr.example.com/about/ https://es.example.com/about/

ccTLD (Strongest geo-targeting signal):

https://example.com/about/ (for US/English) https://example.fr/about/ (for France/French) https://example.de/about/ (for Germany/German)

Don’t rely solely on cookies to switch languages:

Wrong Approach:

GET /about/ HTTP/1.1 Cookie: lang=fr HTTP/1.1 200 OK Content-Type: text/html <!-- Content served in French based on cookie -->

Google’s crawler never sends language cookies, so it only indexes the default (cookie-absent) version.

3. Avoid JavaScript-Only Language Detection

Don’t rely solely on JavaScript to detect and redirect to the correct language:

Wrong Approach:

<script> if (navigator.language.startsWith("fr")) { window.location.href = "/fr/"; } </script>

The crawler does not execute JavaScript, so the redirect never fires during crawling.

4. Use Hreflang with Distinct URLs

Ensure your hreflang tags point to distinct URLs for each language variant:

<link rel="alternate" hreflang="en" href="https://example.com/en/about/" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about/" /> <link rel="alternate" hreflang="es" href="https://example.com/es/about/" />

5. If Using Query Parameters, Ensure They Create Distinct URLs

Query parameters can work but are not Google’s preferred approach:

Acceptable but not optimal:

https://example.com/about/?lang=en https://example.com/about/?lang=fr

Better approach:

https://example.com/en/about/ https://example.com/fr/about/

Priority Order

  1. First: Implement distinct URLs for each language version (subdirectory recommended)
  2. Second: Remove cookie-only or JavaScript-only language switching
  3. Third: Update hreflang tags to point to the new distinct URLs
  4. Fourth: Set up proper redirects from old URLs if migrating from cookie/JS-based switching
  5. Fifth: Ensure all language versions are accessible without requiring cookies or JavaScript

What We Store

Storage Level

Page Level


Database Table / Prisma Model

PageInternationalSeo


Stored Fields

FieldTypeDescription
hasLanguageSpecificUrlsBoolean?Whether language-specific URLs exist

Detection Dependencies

  • HTML Document
  • HTTP Response Headers

Examples

Scenario

A website serves different language versions at the same URL, using cookies to determine which language to display.

Problematic State (Fails)

GET /about/ HTTP/1.1 Host: example.com HTTP/1.1 200 OK Set-Cookie: lang=en; Path=/ Content-Type: text/html <html lang="en"> <!-- English content served based on cookie -->
GET /about/ HTTP/1.1 Host: example.com Cookie: lang=fr HTTP/1.1 200 OK Content-Type: text/html <html lang="fr"> <!-- French content served based on cookie -->

Result: Google’s crawler never sends language cookies, so it only indexes the default (English) version. The French version is invisible to search engines.

Corrected State (Passes)

GET /en/about/ HTTP/1.1 Host: example.com HTTP/1.1 200 OK Content-Type: text/html <html lang="en"> <link rel="alternate" hreflang="en" href="https://example.com/en/about/"> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about/"> <!-- English content -->
GET /fr/about/ HTTP/1.1 Host: example.com HTTP/1.1 200 OK Content-Type: text/html <html lang="fr"> <link rel="alternate" hreflang="en" href="https://example.com/en/about/"> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about/"> <!-- French content -->

Result: Both language versions have distinct, crawlable URLs. Google can index both versions and serve the correct one to users in different regions.


Example 2: JavaScript-Only Language Redirect (Fails)

Scenario

A website uses JavaScript to detect the user’s browser language and redirect to the appropriate language version.

Problematic State (Fails)

<!-- On page: https://example.com/about/ --> <!DOCTYPE html> <html> <head> <script> if (navigator.language.startsWith("fr")) { window.location.href = "/fr/about/"; } else if (navigator.language.startsWith("es")) { window.location.href = "/es/about/"; } </script> </head> <body> <!-- Default English content --> </body> </html>

Result: Google’s crawler does not execute JavaScript, so the redirect never fires. Only the default English content is indexed.

Corrected State (Passes)

<!-- On page: https://example.com/en/about/ --> <!DOCTYPE html> <html lang="en"> <head> <link rel="alternate" hreflang="en" href="https://example.com/en/about/" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about/" /> <link rel="alternate" hreflang="es" href="https://example.com/es/about/" /> </head> <body> <!-- English content served directly from this URL --> </body> </html>

Result: Each language version is served from its own distinct URL without requiring JavaScript execution.


Example 3: Query Parameter Language Switching (Warning)

Scenario

A website uses query parameters to differentiate language versions.

https://example.com/about/?lang=en https://example.com/about/?lang=fr https://example.com/about/?lang=es

Result: While crawlable, this approach is not recommended by Google. Query parameters can be confusing for users and may not be as strongly associated with language targeting as subdirectories.

https://example.com/en/about/ https://example.com/fr/about/ https://example.com/es/about/

Result: Subdirectory approach is Google’s recommended URL structure for multilingual content. It’s clear, crawlable, and strongly signals language targeting.

Unit Test

Test File

__tests__/seo-audit-checks/internationalSeo/issue-171-separate-per-language.test.js

Purpose

This unit test validates that the SEPARATE_PER_LANGUAGE issue is correctly detected when multiple language versions of a page share the same URL instead of using distinct, crawlable URLs per locale.

Tested Function

runInternationalSeo(ctx) from toggleGroups/internationalSeo.js

Issue Information

  • Issue Number: 171
  • Issue Code: separate_per_language
  • Toggle Group: internationalSeo

Test Scenarios

Positive Test Cases

  • Should not detect SEPARATE_PER_LANGUAGE when each language has a separate, unique URL
  • Should not detect issue on a single-language site (only one hreflang tag)
  • Should not detect issue when no hreflang tags exist
  • Should not detect issue when x-default shares a URL with a language tag (x-default is not counted as a language)

Negative Test Cases

  • Should detect SEPARATE_PER_LANGUAGE when multiple languages (en, es, fr) all share the same URL
  • Should detect SEPARATE_PER_LANGUAGE with partial URL overlap (two languages sharing the same URL)

Edge Cases

  • Single-language site: Only one hreflang tag — no URL conflict possible
  • Empty HTML: No hreflang tags — should not detect the issue
  • x-default exclusion: x-default sharing a URL with a language tag should not count as a language conflict
  • Partial URL overlap: Two out of three languages share the same URL — should still detect the issue

Expected Outcome

Pass

The issue should be reported when two or more language-specific hreflang tags (excluding x-default) point to the same URL.

Fail

The issue should not be reported when:

  • Each language has a unique URL
  • Only one language is present
  • No hreflang tags exist
  • Only x-default shares a URL with a language tag (no language conflict)

Validation

The unit test verifies:

  • Correct detection of duplicate URLs across language variants
  • No false positives when all URLs are unique per language
  • x-default is excluded from the URL uniqueness check
  • Correct issue code assignment
  • Metadata (details) is included when the issue is detected
  • toggleGroups/internationalSeo.js — contains runInternationalSeo()
  • issueCodes.js — defines SEPARATE_PER_LANGUAGE issue code constant

Coverage Summary

  • Covers unique URLs per language (pass) and shared URLs (fail)
  • Verifies x-default exclusion from URL uniqueness check
  • Covers partial URL overlap and single-language scenarios
  • Includes empty HTML edge case
  • Validates metadata presence on detection

References

Last updated on