Implement complete multilingual support using Hugo's multilingual mode with German (de) as default and English (en) as secondary language.
Changes:
- Configure Hugo multilingual mode with language-specific parameters
- Reorganize content using language suffix pattern (filename.de.md, filename.en.md)
- Translate all content to English (home page and CV page)
- Implement language switcher that maintains page context
- Add language-specific navigation menus
- Update templates to use language-aware URL functions (relLangURL)
- Fix navbar brand and Home links to respect current language
- Add translationKey to content frontmatter for explicit translation linking
URL Structure:
- German: /de/ (home), /de/cv/ (CV page)
- English: /en/ (home), /en/cv/ (CV page)
Key Implementation Details:
- Use filename.{lang}.md pattern instead of {lang}/filename.md subdirectories
- Always use relLangURL for internal navigation, never .Site.BaseURL
- Menu URLs must include language prefix when defaultContentLanguageInSubdir = true
- Added translationKey to frontmatter for proper translation discovery
Documented 5 critical bugs and their solutions in design.md for future reference.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
11 KiB
Multilingual Support Design
Context
The site needs to support both German and English content with seamless language switching. Hugo provides built-in multilingual support that we'll leverage. The current content is in German only, stored directly in /content/.
Goals / Non-Goals
Goals
- Support German (de) and English (en) languages
- Automatic browser language detection for first-time visitors
- Persistent language preference via URL structure
- Language switcher that maintains page context (e.g.,
/de/cv↔/en/cv) - All existing content translated to English
- Clean URL structure using language codes as path prefixes
Non-Goals
- Translation management system or CMS integration
- More than 2 languages at this stage
- Client-side JavaScript for language switching (Hugo server-side only)
- Automated translation (manual translation required)
Decisions
Decision 1: Hugo Multilingual Mode
Approach: Use Hugo's built-in multilingual mode with language code prefixes in URLs
Rationale:
- Hugo natively supports multilingual sites with
defaultContentLanguageand[languages]config - URL structure
/de/and/en/clearly indicates language and persists preference - No cookies or JavaScript needed for language persistence
- SEO-friendly with proper
hreflangtags
Alternatives Considered:
- Subdomain approach (de.markusgraf.ch, en.markusgraf.ch): Rejected due to complexity and DNS configuration
- Client-side JavaScript switching: Rejected to keep site static and performant
Decision 2: Content Organization
Approach: Reorganize content into language-specific directories /content/de/ and /content/en/
Structure:
content/
├── de/
│ ├── _index.md
│ └── cv.md
└── en/
├── _index.md
└── cv.md
Rationale:
- Clear separation of language-specific content
- Hugo automatically links translations when file paths match
- Easy to maintain and scale
Decision 3: Language Switcher Implementation
Approach: Add language switcher in navigation partial using Hugo's .Translations function
Implementation:
- Display language switcher as links (e.g., "DE | EN")
- Active language styled differently
- Maintains current page context when switching
- Falls back to home page if translation doesn't exist
Rationale:
- Simple, no JavaScript required
- Hugo provides
.Translationsto find corresponding pages - Consistent with existing navigation patterns
Decision 4: Default Language
Approach: German (de) as default with English (en) as secondary
Configuration:
defaultContentLanguage = "de"
defaultContentLanguageInSubdir = true
Rationale:
- Existing content is in German
- Primary audience is German-speaking
defaultContentLanguageInSubdir = trueensures consistent URLs (/de/for German, not just/)
Decision 5: Browser Language Detection
Approach: Hugo serves content based on URL path; Apache/Nginx handles initial redirect based on Accept-Language header
Rationale:
- Hugo is a static site generator and cannot detect browser language at runtime
- Server-side redirect is the cleanest approach
- Falls back to default language (German) if header not present
- Once user navigates, URL path maintains their choice
Migration Path:
- Configure multilingual mode in hugo.toml
- Reorganize existing content to
/content/de/ - Create English translations in
/content/en/ - Update templates with language switcher
- Configure server redirect rules (optional, for initial visit only)
Risks / Trade-offs
Risk 1: Breaking Change in Content Structure
Risk: Moving content files breaks existing local references and workflows Mitigation:
- Clear documentation of new structure
- All content moved in single commit
- Update paths in OpenSpec documentation
Risk 2: Translation Maintenance
Risk: Keeping German and English content in sync over time Mitigation:
- Document translation process
- Consider adding translation status tracking in future
- For now, manual process with clear ownership
Risk 3: SEO Impact
Risk: URL structure changes may affect search rankings Mitigation:
- Implement proper
hreflangtags (Hugo does this automatically) - Add redirects from old URLs to new German URLs
- Submit new sitemap to search engines
Migration Plan
Phase 1: Configure Hugo (No Breaking Changes Yet)
- Update hugo.toml with multilingual configuration
- Test configuration with existing content structure
Phase 2: Restructure Content
- Move existing content to
/content/de/ - Update any internal references
- Build and verify German site works at
/de/
Phase 3: Add English Content
- Create
/content/en/directory - Translate home page content
- Translate CV page content
- Translate navigation and UI strings
Phase 4: Update Templates
- Add language switcher to navigation
- Update partials for multilingual strings
- Test language switching functionality
Phase 5: Server Configuration (Optional)
- Add .htaccess or nginx rules for browser language detection
- Redirect root
/to/de/or/en/based on Accept-Language header
Open Questions
Q: Should the root URL / redirect to a language version, or show a language selection page?
A: Redirect to /de/ as default, optionally detect browser language. No standalone language selection page needed.
Q: How should we handle the menu configuration for different languages?
A: Hugo supports language-specific menus using [[languages.de.menu.main]] and [[languages.en.menu.main]] in hugo.toml.
Q: Should we translate the project descriptions on the home page? A: Yes, translate all visible content including project descriptions.
Implementation Bugs and Solutions
During implementation, several critical bugs were discovered and fixed. These are documented here to avoid future pitfalls.
Bug 1: Language Switcher Showing Current Language
Problem: The language switcher initially showed a dropdown with the current language as the toggle button (e.g., showing "Deutsch" when on German pages). This was confusing because users couldn't easily see which language to switch to.
Solution: Simplified the language switcher to only show the alternative language as a direct link. Removed the dropdown pattern and changed from:
<li class="nav-item dropdown">
<a class="dropdown-toggle">{{ .Language.LanguageName }}</a>
<ul class="dropdown-menu">...</ul>
</li>
To:
{{ if .IsTranslated }}
{{ range .Translations }}
<li class="nav-item">
<a class="nav-link" href="{{ .RelPermalink }}">{{ .Language.LanguageName }}</a>
</li>
{{ end }}
{{ end }}
Lesson: For a two-language site, showing only the alternative language is clearer than a dropdown showing the current language.
Bug 2: Language Switcher Not Maintaining Page Context
Problem: When switching languages from /de/cv/ to English, it would redirect to /en/ (home) instead of /en/cv/ (maintaining the CV page context).
Solution: Added translationKey to the frontmatter of both language versions of each content file to explicitly link them as translations:
---
title: "Curriculum Vitae"
translationKey: "cv"
---
This allows Hugo's .IsTranslated and .Translations functions to correctly find corresponding pages.
Lesson: Hugo doesn't automatically recognize translations by filename alone when using language subdirectories. Always add translationKey to explicitly link translated pages.
Bug 3: CV Pages Rendering with Wrong Language Context
Problem: The most critical bug - CV pages would render with the wrong language:
/en/cv/would showlang="de", German menu ("Lebenslauf"), and German language context- Both German and English CV pages would show incorrect language switcher labels
Root Cause: Using language subdirectories (content/de/cv.md and content/en/cv.md) caused Hugo to incorrectly resolve the language context for these pages, even though hugo list all showed them with the correct language tags.
Solution: Reorganized content files to use Hugo's language suffix naming convention instead of subdirectories:
- Changed from:
content/de/cv.mdandcontent/en/cv.md - Changed to:
content/cv.de.mdandcontent/cv.en.md
After this change, all pages rendered with the correct language context.
Lesson: With Hugo's multilingual mode and defaultContentLanguageInSubdir = true, use the .{lang}.md suffix pattern (e.g., filename.de.md, filename.en.md) rather than language subdirectories (de/filename.md, en/filename.md). The subdirectory approach can cause template rendering issues where the wrong language context is used.
Bug 4: Home and Navbar Brand Not Respecting Language
Problem: Clicking the "Home" link or navbar brand from /en/cv/ would always redirect to / (German home) instead of /en/ (English home), breaking language persistence.
Root Cause: Both links used {{ .Site.BaseURL }} which always points to the absolute root URL without language awareness.
Solution: Changed both to use Hugo's relLangURL function:
<!-- Before -->
<a href="{{ .Site.BaseURL }}">Home</a>
<!-- After -->
<a href="{{ "/" | relLangURL }}">Home</a>
This makes the links language-aware, so:
- On German pages:
{{ "/" | relLangURL }}→/de/ - On English pages:
{{ "/" | relLangURL }}→/en/
Lesson: Never use {{ .Site.BaseURL }} for internal navigation in multilingual sites. Always use relLangURL or absLangURL to maintain language context.
Bug 5: Menu URLs Not Language-Aware
Problem: The German "Lebenslauf" menu link pointed to /cv/ which resulted in a 404 error because with defaultContentLanguageInSubdir = true, German pages are at /de/cv/, not /cv/.
Root Cause: Menu URLs in hugo.toml were hardcoded:
[[languages.de.menu.main]]
url = '/cv/' # Wrong - page doesn't exist here
Solution: Updated menu URLs to include the language prefix:
[[languages.de.menu.main]]
name = 'Lebenslauf'
url = '/de/cv/' # Correct
weight = 10
[[languages.en.menu.main]]
name = 'Curriculum Vitae'
url = '/en/cv/' # Correct
weight = 10
Lesson: When defaultContentLanguageInSubdir = true, even the default language requires the language prefix in all URLs. Don't assume the default language is accessible at the root paths.
Best Practices Derived from Bugs
-
Content Organization: Use
filename.{lang}.mdpattern instead of{lang}/filename.mdsubdirectories to avoid template context issues. -
Translation Linking: Always add
translationKeyto frontmatter for explicit translation relationships. -
URL Generation: Use Hugo's language-aware functions:
relLangURLfor relative URLsabsLangURLfor absolute URLs.RelPermalinkfor page permalinks- Never use
{{ .Site.BaseURL }}for internal links
-
Menu Configuration: When
defaultContentLanguageInSubdir = true, all menu URLs must include the language prefix, even for the default language. -
Testing: Always test all pages in all languages, not just home pages. CV/subpages may render with different template contexts than home pages.