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
Examples assume
defaultLang = "en"andshowDefaultLang = false. These URLs adapt automatically when you change these settings.
| 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
import { buildLocalizedStaticPaths } from "@i18n/utils";
// Single-file: [...pages].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/pages", ["...pages"]);
}
Pattern B: [pages]/[...index].astro
import { buildLocalizedStaticPaths } from "@i18n/utils";
// Folder pattern: [pages]/[...index].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/pages", ["pages", "...index"]);
}
βοΈ 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",
"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 heredefaultLang- sets the default application language; flipping this moves the root (/) to that languageshowDefaultLang- toggles whether the default language appears in URLs (e.g.,/en/instead of/)
These are fully supported across pages, blog, and pagination.
export const languages = {
en: "English",
sl: "Slovenian",
};
export const defaultLang = "en"; // set to "sl" to make Slovenian the root
export const showDefaultLang = false; // true β always prefix: /en/..., /sl/...
How to flip the default language
- Open
src/i18n/ui.tsand set:defaultLang = "sl"(or your new default)showDefaultLang = true | falsedepending on whether you want a prefix for the default language
- The root becomes:
/whenshowDefaultLang = false/{defaultLang}/whenshowDefaultLang = true
- No other changes are needed: all pages use
useTranslatedPath()ingetStaticPaths()so URLs adapt automatically. - Sanity check a few URLs (home, about, blog list/post, pagination) and the language switcher.
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");
π§© Helper: buildLocalizedStaticPaths()
Generates localized getStaticPaths() entries from an English base path and a simple param pattern. It translates the base path for each language, splits it into segments, and maps those segments into Astro route params.
Signature:
function buildLocalizedStaticPaths(
basePath: string,
pattern: string[],
extraProps?: (lang: string) => Record<string, any>
): Array<{ params: Record<string, string | undefined>; props: Record<string, any> }>
Pattern rules:
- Fixed param names: βaboutβ, βdyn_routingβ, βsubpage2β
- Trailing catchβall: prefix with
...and put it last (e.g., ββ¦indexβ, ββ¦subpage1β, ββ¦blogβ) - Missing segments become
undefinedautomatically (as Astro expects) - Respects
defaultLangandshowDefaultLang
Examples:
// About folder: [about]/[...index].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/about", ["about", "...index"]);
}
// Dynamic Routing root: [dyn_routing]/[...index].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/dynamic-routing", ["dyn_routing", "...index"]);
}
// Dynamic Routing subpage 1: [dyn_routing]/[...subpage1].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/dynamic-routing/subpage-1", ["dyn_routing", "...subpage1"]);
}
// Dynamic Routing subpage 2 (nested): [dyn_routing]/[subpage2]/[...index].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/dynamic-routing/subpage-2", ["dyn_routing", "subpage2", "...index"]);
}
// Blog listing: [...blog].astro
export function getStaticPaths() {
return buildLocalizedStaticPaths("/blog", ["...blog"]);
}
// Optional: extra props per language
export function getStaticPaths() {
return buildLocalizedStaticPaths("/services", ["services", "...index"], (lang) => ({ section: "services", lang }));
}
Note (pagination): the pagination route uses this helper to resolve the localized base (e.g., blog-pagination β spletni-dnevnik-paginacija) and then fans out pages with paginate() while preserving the i18n URL shape.
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, buildLocalizedStaticPaths } from "@i18n/utils";
export function getStaticPaths() {
return buildLocalizedStaticPaths("/services", ["services", "...index"]);
}
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 not use Astroβs built-in i18n?
Astro doesnβt provide fully localized, per-segment URLs with content-linked slugs and the precise prefix behavior used here (defaultLang/showDefaultLang + route translations). This project implements a small i18n layer (routes.ts, useTranslatedPath, switchLanguageUrl) that guarantees:
- Translated route segments (e.g.,
/aboutβ/o-projektu) - Optional prefix for the default language
- Language switching that preserves context, including blog slugs via
linkedContent
How do I add a new language?
- Add the language to
src/i18n/ui.ts(languages). - Create translation files in
src/locales/[lang]/.... - Add route mappings in
src/i18n/routes.tsfor any segments you localize.
No changes to page files are required β all getStaticPaths() use useTranslatedPath() and automatically pick up the new language.
How does language switching work for blog posts?
Each post has a linkedContent identifier. switchLanguageUrl() uses this to find the matching post in the target language, translate the base route (blog β spletni-dnevnik), and apply the correct prefix depending on showDefaultLang.
Can I use relative paths?
Use absolute paths with a leading slash (/about, not about). Path translation relies on consistent absolute segments.
π― This approach enables fully localized URLs with static generation performance and predictable SEO-friendly paths.