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/
βββ actions/ # Astro Actions (contact form handler)
β βββ index.ts
βββ assets/ # Logo and other bundled assets
βββ components/ # Components (Header, Footer, LanguagePicker, Pagination, Counter.svelte)
βββ content/ # Content collections (blog, authors)
β βββ blog/
β β βββ en/ # English posts
β β βββ sl/ # Slovenian posts
β β βββ images/ # Post images
β βββ authors/ # Author data (authors.json)
βββ content.config.ts # Collection schemas (blog, authors)
βββ data/ # Navigation data + robots.txt templates
β βββ navigationData.ts
β βββ robots/
βββ i18n/ # Internationalization system
β βββ routes.ts # Path translations
β βββ ui.ts # Language configuration
β βββ utils.ts # Utility functions
βββ layouts/ # Base layout (head, hreflang, SEO)
βββ 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
β βββ [contact]/ # Contact page (Astro Actions form)
β β βββ [...index].astro
β βββ [dyn_routing]/ # Dynamic routing examples
β β βββ [subpage2]/
β β β βββ [...index].astro
β β βββ [...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
β βββ [...index].astro # Home page
β βββ 404.astro # Error page
β βββ robots.txt.ts # robots.txt endpoint
βββ styles/ # Style files
βββ utils/ # Helpers (formatDate)
π 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]/[...index].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.mdx
β
βββ Slovenian path
βββ URL: /sl/o-projektu
βββ Parameters: { about: "sl", index: "o-projektu" }
βββ Props: { lang: "sl" }
βββ Content: _about-sl.mdx
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. The default language (en) keeps the original route names, so it has no entry:
export const routes: Record<string, Record<string, string>> = {
sl: {
about: "o-projektu",
blog: "spletni-dnevnik",
contact: "kontakt",
"dynamic-routing": "dinamicno-usmerjanje",
"blog-pagination": "spletni-dnevnik-paginacija",
pages: "strani",
"subpage-1": "podstran-1",
"subpage-2": "podstran-2",
},
};
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 herelocales- BCP-47 tag per language (en-US,sl-SI), used forIntldate formatting (formatDate) and theog:localemeta tagdefaultLang- 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 locales: Record<keyof typeof languages, string> = {
en: "en-US",
sl: "sl-SI",
};
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.services",
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, 404)
β βββ main.json # Home page
β βββ blog.json # Blog pages
β βββ routing.json # Dynamic routing examples
β βββ contact.json # Contact form + validation messages
β βββ component.json # Svelte component example
βββ sl/
βββ common.json
βββ main.json
βββ blog.json
βββ routing.json
βββ contact.json
βββ component.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-2025.md):
---
title: "Top Security Trends for 2025"
description: "A deep dive into the cybersecurity challenges and solutions defining the digital landscape in 2025."
linkedContent: "security-trends-2025"
author: "Nik Klemenc" # id from authors.json
pubDate: 2025-03-05
isDraft: false
image: "@blogImages/image-2.png"
---
Slovenian post (sl/varnostni-trendi-2025.md):
---
title: "Glavni varnostni trendi za leto 2025"
description: "Poglobljeno raziskovanje kibernetskih varnostnih izzivov in reΕ‘itev, ki opredeljujejo digitalno pokrajino leta 2025."
linkedContent: "security-trends-2025" # Same identifier!
author: "Nik Klemenc"
pubDate: 2025-03-05
isDraft: false
image: "@blogImages/image-2.png"
---
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. The id is what blog posts reference in their author field (reference("authors") in src/content.config.ts).
[
{
"id": "Nik Klemenc",
"name": "Nik Klemenc",
"image": "./user.png",
"position": {
"en": "UX/UI Designer & Front-End Developer",
"sl": "UX/UI Oblikovalec & Front-end 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
The picker navigates with navigate() from astro:transitions/client, so switching languages keeps the <ClientRouter /> view transitions instead of forcing a full page reload.
---
// 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.keys(languages).map(async (lang) => {
const targetUrl = await switchLanguageUrl(Astro.url, lang);
const translatedLabel = t(`menu.languages.${lang}`);
return { lang, label: translatedLabel, targetUrl };
})
);
---
<!-- Dropdown selector -->
<select
name="language"
id="language"
aria-label={t("menu.languagesText.selectLanguage")}
>
{languageUrls.map(({ lang, label, targetUrl }) => (
<option
value={targetUrl}
selected={lang === currentLang}
>
{label}
</option>
))}
</select>
<script>
import { navigate } from "astro:transitions/client";
// Delegated listener survives <ClientRouter /> page swaps;
// navigate() keeps view transitions instead of a full reload
document.addEventListener("change", (event) => {
const el = event.target;
if (el instanceof HTMLSelectElement && el.id === "language") {
navigate(el.value);
}
});
</script>
3. Localized Links
---
import { getLangFromUrl, useTranslations, useTranslatedPath } from "@i18n/utils";
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
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
languagesandlocales(BCP-47 tag) insrc/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.