Dynamic Routing Documentation with i18n
Since Astro doesnβt natively support URL localization out of the box, a different approach was needed to enable multilingual pages. This project uses dynamic parameters ([...pages]) to implement localized paths.
ποΈ Project Architecture Overview
The project implements an internationalization (i18n) system that enables:
- β SEO-friendly URLs in multiple languages
- β Automatic static generation at build time
- β Language-specific content loading
- β Seamless integration with translation system
- β Smart language switching with context preservation
π File Structure
src/
βββ components/ # Components (Header, Footer, LanguagePicker)
βββ content/ # Content (blogs, authors)
β βββ blog/
β β βββ en/ # English posts
β β βββ sl/ # Slovenian posts
β βββ authors/ # Author data
βββ data/ # Navigation data
β βββ navigationData.ts
βββ i18n/ # Internationalization system
β βββ routes.ts # Path translations
β βββ ui.ts # Language configuration
β βββ utils.ts # Utility functions
βββ layouts/ # Base layouts
βββ locales/ # Translation files
β βββ en/ # English translations
β βββ sl/ # Slovenian translations
βββ pages/ # Pages with dynamic routing
β βββ [about]/ # About pages
β β βββ [...index].astro
β β βββ _about-en.mdx
β β βββ _about-sl.mdx
β βββ [blog]/ # Blog detail pages
β β βββ [...slug].astro
β βββ [dyn_routing]/ # Dynamic routing examples
β β βββ [subpage2]/
β β β βββ [...index].astro
β β βββ [...subpage1].astro
β βββ [pages]/ # General pages
β β βββ [...index].astro
β β βββ _pages-en.mdx
β β βββ _pages-sl.mdx
β βββ [pagination]/ # Pagination examples
β β βββ [...page].astro
β βββ [...blog].astro # Blog listing page
β βββ [...contact].astro # Contact page
β βββ [...index].astro # Home page
β βββ 404.astro # Error page
βββ styles/ # Style files
π How Dynamic Routing Works
1. URL Structure
| Page Pattern | English URL | Slovenian URL | Description |
|---|---|---|---|
[...index].astro | / | /sl/ | Home page |
[about]/[...index].astro | /about | /sl/o-projektu | About |
[...contact].astro | /contact | /sl/kontakt | Contact |
[blog]/[...slug].astro | /blog/post | /sl/spletni-dnevnik/objava | Blog posts |
2. Visual Routing Flow
File: [about]/[...index].astro
β
βββ English path
β βββ URL: /about
β βββ Parameters: { about: "about", index: undefined }
β βββ Props: { lang: "en" }
β βββ Content: _about-en.md
β
βββ Slovenian path
βββ URL: /sl/o-projektu
βββ Parameters: { about: "sl", index: "o-projektu" }
βββ Props: { lang: "sl" }
βββ Content: _about-sl.md
3. Page Structures
There are two main patterns for implementing dynamic pages:
Pattern A: [...pages].astro
export function getStaticPaths() {
return [
// English path: /pages
// [...pages] = "/pages" captures the entire path segment
// Creates URL: /pages
{ params: { pages: "/pages" }, props: { lang: "en" } },
// Slovenian path: /sl/strani
// [...pages] = "sl/strani" captures language prefix and localized path
// Creates URL: /sl/strani ("strani" = "pages" in Slovenian)
{ params: { pages: `/sl/strani` }, props: { lang: "sl" } },
];
}
Pattern B: [pages]/[...index].astro
export function getStaticPaths() {
return [
// English path: /pages
// [pages]/[...index.astro] captures the entire path segment
// English: { params: { pages: "pages", index: undefined }, props: { lang: "en" } } β /pages
{ params: { pages: "pages", index: undefined }, props: { lang: "en" } },
// Slovenian path: sl/strani
// [pages]/[...index.astro] captures the entire path segment
// Slovenian: { params: { pages: "sl", index: "strani" }, props: { lang: "sl" } } β /sl/strani
{ params: { pages: "sl", index: "strani" }, props: { lang: "sl" } },
];
}
βοΈ Key System Components
1. src/i18n/routes.ts - Path Translations
This file defines mappings between English and localized paths:
export const routes: Record<string, Record<string, string>> = {
sl: {
about: "o-projektu",
blog: "spletni-dnevnik",
services: "storitve",
pages: "strani",
contact: "contact",
},
};
Important: Keys must match parameters in
getStaticPaths()functions!
2. src/i18n/ui.ts - Language Configuration
This file contains key configurations for language support:
languages- defines supported languages and their labels; add new languages heredefaultLang- sets the default application language (currently not dynamically changeable)showDefaultLang- controls whether the default language appears in URLs (e.g., β/en/β instead of β/β)
Note: The
defaultLangandshowDefaultLangfunctionalities are not fully implemented and represent opportunities for future development.
export const languages = {
en: "English",
sl: "Slovenian",
};
export const defaultLang = "en";
export const showDefaultLang = false;
3. src/i18n/utils.ts - Utility Functions
getLangFromUrl(url: URL)
Extracts the current language from the URL:
const lang = getLangFromUrl(Astro.url); // "sl" or "en"
useTranslations(lang: string)
Returns a function for translating texts:
const t = useTranslations(lang);
const title = t("main:head.title");
const navHome = t("menu.list.home"); // Uses "common" namespace
Supported key formats:
"namespace:key"βt("main:title")- Direct keys β
t("menu.home")(defaults to βcommonβ namespace) - Parameters β
t("footer.made", { what: "Astro" })
useTranslatedPath(lang: string)
Returns a function for translating paths:
const translatePath = useTranslatedPath(lang);
<a href={translatePath("/about")}>About</a>; // "/o-projektu" for Slovenian
switchLanguageUrl(currentUrl: URL, targetLang: string)
Enables language switching while preserving current page context:
const newUrl = await switchLanguageUrl(Astro.url, "sl");
4. src/data/navigationData.ts - Navigation
const navigationData = [
{
label: "menu.list.home", // Translation key
href: "/", // English path (default)
children: [],
},
{
label: "menu.list.service",
href: "/services",
children: [
{
label: "menu.list.subpage-1",
href: "/services/service-1",
},
],
},
];
Note: URLs in navigation are always in English because theyβre automatically localized via
translatePath().
π£οΈ Translation System
1. Translation File Structure
src/locales/
βββ en/
β βββ common.json # Shared translations (navigation, footer)
β βββ main.json # Main page
β βββ about.json # About page
β βββ blog.json # Blog page
βββ sl/
βββ common.json
βββ main.json
βββ about.json
βββ blog.json
2. Translation File Example (common.json)
{
"menu": {
"list": {
"home": "Home",
"about": "About",
"blog": "Blog"
}
},
"footer": {
"made": "Made with {{what}}"
}
}
Note: For
common.jsonfiles, you donβt need to prefix withcommon:. Uset("menu.list.home")directly, nott("common:menu.list.home").
3. Using Translations in Components
---
import { useTranslations } from "@i18n/utils";
const t = useTranslations(lang);
---
<h1>{t("main:hero.title")}</h1>
<p>{t("menu.list.home")}</p>
<footer>{t("footer.made", { what: "Astro" })}</footer>
π Blog System with linkedContent
1. Linking Content Between Languages
The blog system enables linking posts between different languages. The key question is: how does the system know the user is still on the same post when switching languages?
π Solution: Each blog post must have a linkedContent identifier in the frontmatter. This identifier connects posts in different languages that cover the same topic. When users switch languages, the system uses this identifier to find the corresponding post in the target language.
Each blog post must have a linkedContent identifier in the frontmatter:
English post (en/security-trends.md):
---
title: "Top Security Trends for 2025"
linkedContent: "security-trends-2025"
author: "Nik Klemenc"
---
Slovenian post (sl/varnostni-trendi-2025.md):
---
title: "Glavni varnostni trendi za leto 2025"
linkedContent: "security-trends-2025" # Same identifier!
author: "Nik Klemenc"
---
2. Authors with Multilingual Data
The system enables linking authors with blog posts. In this case, a JSON format is used where each author has multilingual data. Under the βpositionβ key, positions are defined in English and Slovenian, which are then dynamically displayed based on the selected language.
{
"nik-klemenc": {
"name": "Nik Klemenc",
"image": "./nik.jpg",
"position": {
"en": "Full-stack Developer",
"sl": "Full-stack razvijalec"
}
}
}
π Pagination
The project includes an example of pagination in [pagination]/[...page].astro, implemented using Astroβs default paginate() and adapted for localized URLs.
// Pagination with Astro's paginate()
export const getStaticPaths = async ({ paginate }) => {
const posts = /* fetch posts and sort per language */ [];
return paginate(posts, {
pageSize: 4,
params: { pagination: "blog-pagination" },
props: {
/* lang, authors, totals */
},
});
};
- URLs: EN
/blog-pagination,/blog-pagination/2; SL/sl/spletni-dnevnik-paginacija,/sl/spletni-dnevnik-paginacija/2. - The template uses
page.data,page.currentPage, andpage.lastPage.
π» Usage Examples
1. Basic Page with Dynamic Routing
---
import { useTranslations } from "@i18n/utils";
export function getStaticPaths() {
return [
{ params: { pages: "/services" }, props: { lang: "en" } },
{ params: { pages: "/sl/storitve" }, props: { lang: "sl" } }
];
}
const { lang } = Astro.props;
const t = useTranslations(lang);
---
<Base title={t("services:head.title")}>
<h1>{t("services:title")}</h1>
</Base>
2. Language Switching
---
// LanguagePicker.astro - actual implementation
import { switchLanguageUrl, getLangFromUrl, useTranslations } from "@i18n/utils";
import { languages } from "@i18n/ui";
// Get current language
const currentLang = getLangFromUrl(Astro.url);
const t = useTranslations(currentLang);
// Prepare URLs for all languages
const languageUrls = await Promise.all(
Object.entries(languages).map(async ([lang, label]) => {
const targetUrl = await switchLanguageUrl(Astro.url, lang);
const translatedLabel = t(`menu.languages.${lang}`);
return { lang, label: translatedLabel, targetUrl };
})
);
---
<!-- Dropdown selector -->
<select
name="language"
onchange="window.location.href = this.value"
aria-label={t("menu.languagesText.selectLanguage")}
>
{languageUrls.map(({ lang, label, targetUrl }) => (
<option
value={targetUrl}
selected={lang === currentLang}
>
{label}
</option>
))}
</select>
3. Localized Links
---
import { useTranslatedPath } from "@i18n/utils";
const translatePath = useTranslatedPath(lang);
---
<a href={translatePath("/about")}>
{t("menu.list.about")}
</a>
β¨ Best Practices
1. File Naming
- Use English names for files in the
pages/directory - Localize only URLs via
routes.ts - Examples:
[about]folder, not[o-projektu]
2. Translation Keys
- Use hierarchical structure (
menu.list.home) - Separate by namespaces (
main:title,about:description) - Add context to key names
3. Content Linking
- Always add
linkedContentidentifier to blog posts - Use consistent naming between languages
- Test language switching on all pages
4. SEO Optimization
- Add
titleanddescriptionmeta data - Use
hreflangattributes for multilingual pages - Implement structured data
β Frequently Asked Questions
Why donβt you use Astroβs built-in i18n functionality?
Although Astro supports internationalization, it doesnβt support βout of the boxβ URL localization (e.g., /about β /sl/o-projektu). This project implements fully localized URLs with dynamic parameters, enabling complete control over path structure and SEO optimization.
How do I add a new language?
- Add the language to
src/i18n/ui.ts - Create a translation folder in
src/locales/[lang]/ - Add path mappings in
src/i18n/routes.ts - Update
getStaticPaths()in all page files
How does language switching work for blog posts?
The system uses linkedContent identifiers to link posts between languages. The switchLanguageUrl() function automatically finds the corresponding post in the target language.
Can I use relative paths?
No, always use absolute paths with a leading slash (/about, not about). The translation system relies on consistent path structure.
π― This approach enables complete localization of Astro applications while maintaining static generation speed and SEO optimization.