URL & Language Routing
When to Use
When configuring multilingual URLs — path prefixes, domains, path aliases, hreflang tags for SEO.
Decision: URL Structure
| If you need... | Use... | Example URLs |
|---|---|---|
| Path prefix (most common) | URL negotiation, source: path_prefix | /en/about, /es/acerca |
| No prefix for default language | Empty prefix for default | /about, /es/acerca |
| Language-specific domains | URL negotiation, source: domain | en.example.com/about, es.example.com/about |
| Query parameter (not recommended) | Session negotiation | /about?language=es |
Recommendation: Path prefix with no prefix for default language. Best for SEO and user experience.
Pattern: Path Prefix Configuration
File: config/install/language.negotiation.yml
url:
source: path_prefix
prefixes:
en: '' # No prefix for English (default)
es: es # /es/ for Spanish
fr: fr # /fr/ for French
domains:
en: ''
Resulting URLs:
- English: https://example.com/about
- Spanish: https://example.com/es/about
- French: https://example.com/fr/about
Path aliases work per language:
- English: /about (alias for /node/1)
- Spanish: /es/acerca (alias for /node/1 in Spanish)
- French: /fr/a-propos (alias for /node/1 in French)
Pattern: Domain-Based Configuration
File: config/install/language.negotiation.yml
url:
source: domain
prefixes:
en: ''
es: ''
fr: ''
domains:
en: 'example.com'
es: 'es.example.com'
fr: 'fr.example.com'
Resulting URLs:
- English: https://example.com/about
- Spanish: https://es.example.com/about
- French: https://fr.example.com/about
Requirements: - DNS configuration for subdomains - SSL certificates for each domain - Web server configuration (virtual hosts)
Pattern: Hreflang Tags (SEO)
Drupal automatically adds hreflang tags when: - Language module enabled - Multiple languages configured - Content has translations
Generated HTML (automatic):
<link rel="alternate" hreflang="en" href="https://example.com/about" />
<link rel="alternate" hreflang="es" href="https://example.com/es/acerca" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
What hreflang does:
- Tells Google/Bing which language version to show
- Prevents duplicate content penalties
- Improves international SEO
- x-default specifies default for unmatched languages
Manual override (if needed):
// In hook_page_attachments_alter()
function mymodule_page_attachments_alter(&$attachments) {
$attachments['#attached']['html_head_link'][] = [
[
'rel' => 'alternate',
'hreflang' => 'es',
'href' => 'https://example.com/es/custom-path',
],
];
}
Pattern: Language-Aware Path Aliases
Via UI:
- Edit node → URL alias tab
- Enter alias per language
- English: /about-us
- Spanish: /acerca-de
Via Pathauto (contrib):
# Pattern per language
/admin/config/search/path/patterns
- English pattern: /[node:menu-link:parents:join-path]/[node:title]
- Spanish pattern: /[node:menu-link:parents:join-path]/[node:title]
Pathauto generates language-specific aliases using translated tokens.
Pattern: Language Switcher Block
Enable block: /admin/structure/block
- Place "Language switcher" block in desired region
- Configure to show links for all languages
Generated links:
<ul class="language-switcher">
<li class="en"><a href="/about" hreflang="en">English</a></li>
<li class="es"><a href="/es/acerca" hreflang="es">Español</a></li>
<li class="fr"><a href="/fr/a-propos" hreflang="fr">Français</a></li>
</ul>
Custom language switcher in Twig (requires preprocess):
// In mytheme.theme or mymodule.module
function mytheme_preprocess_block__language_block(&$variables) {
$language_manager = \Drupal::languageManager();
$current = $language_manager->getCurrentLanguage()->getId();
$languages = $language_manager->getLanguages();
$variables['languages'] = $languages;
$variables['current_langcode'] = $current;
}
{# In block--language-block.html.twig #}
<ul>
{% for langcode, language in languages %}
<li{{ langcode == current_langcode ? ' class="is-active"' }}>
<a href="{{ path('<current>', {}, {'language': language}) }}" hreflang="{{ langcode }}">
{{ language.getName() }}
</a>
</li>
{% endfor %}
</ul>
Common Mistakes
- Forgetting to configure URL prefixes → Enabling URL negotiation without setting prefixes causes all languages to use same URL, breaking caching
- Using same alias for multiple languages → Path aliases must be unique per language. Same alias in English and Spanish causes conflicts
- Not testing with clean URLs disabled → Language prefixes require clean URLs. Testing with query strings (
?q=) can cause unexpected behavior - Assuming hreflang works without translations → Hreflang tags only appear if content has translations. Single-language content has no hreflang
- Missing x-default hreflang → Google recommends
x-defaultfor fallback. Drupal adds automatically if default language configured - Wrong domain in hreflang tags → Domain-based setup requires correct domain in hreflang. Verify generated tags match actual domains
See Also
- → Language Negotiation — URL detection priority
- → Security, Performance & Caching — language cache contexts
- Reference:
core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php - Reference: https://www.specbee.com/blogs/multilingual-seo-and-hreflang-how-drupal-makes-it-easier
- Reference: https://pantheon.io/learning-center/drupal/multilingual