Skip to main content

Overview

Chapinismos uses Astro’s file-based routing system with dynamic parameters for multilingual support and word pages.

File-Based Routing

Astro automatically creates routes based on files in src/pages/:
src/pages/
├── index.astro                    → /
├── [lang]/
│   ├── index.astro                → /es/, /en/
│   ├── buscar.astro               → /es/buscar/, /en/buscar/
│   ├── indice.astro               → /es/indice/, /en/indice/
│   └── palabras/
│       └── [slug].astro           → /es/palabras/*, /en/palabras/*

Root Redirect Page

The root index.astro detects browser language and redirects.
---
// Disable prerendering for server-side language detection
export const prerender = false;

// Server-side language detection and redirect
const acceptLanguage = Astro.request.headers.get("Accept-Language") || "";
const lang = acceptLanguage.toLowerCase().includes("es") ? "es" : "en";

// Use 302 (temporary) redirect for language-based routing
return Astro.redirect(`/${lang}/`, 302);
---
This is the only page with prerender: false to enable server-side rendering for dynamic language detection.

Dynamic Language Routes

Pages under [lang]/ use the [lang] parameter to generate routes for both Spanish and English.

Static Path Generation

Use getStaticPaths() to define which language routes to generate:
---
export const prerender = true;

import Base from "../../layouts/Base.astro";
import { getCollection } from "astro:content";
import { useTranslations } from "../../utils/i18n";

export async function getStaticPaths() {
  return [
    { params: { lang: "es" } },
    { params: { lang: "en" } }
  ];
}

const { lang } = Astro.params;
const t = useTranslations(lang);

const collectionName = lang === "es" ? "words-es" : "words-en";
const allWords = await getCollection(collectionName);
---

<Base title={t("home.title")} lang={lang}>
  <!-- Page content -->
</Base>

Double Dynamic Routes

The word detail page uses both [lang] and [slug] parameters.

Path: src/pages/[lang]/palabras/[slug].astro

Generates routes like:
  • /es/palabras/chispa/
  • /en/palabras/chispa/
  • /es/palabras/chunche/
---
export const prerender = true;

import Base from "../../../layouts/Base.astro";
import { getCollection } from "astro:content";
import { useTranslations } from "../../../utils/i18n";

export async function getStaticPaths() {
  const esWords = await getCollection("words-es");
  const enWords = await getCollection("words-en");

  const esPaths = esWords.map((entry) => ({
    params: { lang: "es", slug: entry.slug },
    props: { entry },
  }));

  const enPaths = enWords.map((entry) => ({
    params: { lang: "en", slug: entry.slug },
    props: { entry },
  }));

  return [...esPaths, ...enPaths];
}

const { lang } = Astro.params;
const { entry } = Astro.props;
const word = entry.data;

const t = useTranslations(lang);
---

<Base title={`${word.word}${t("word.page.title")}`} lang={lang}>
  <article>
    <h1>{word.word}</h1>
    <p>{word.meaning}</p>
    <!-- More content -->
  </article>
</Base>

Page Types

Homepage ([lang]/index.astro)

Main landing page with hero, search, and featured words.

Search Page ([lang]/buscar.astro)

Client-side search interface for finding words.

Index Page ([lang]/indice.astro)

Alphabetical listing of all words.

Word Detail Page ([lang]/palabras/[slug].astro)

Individual word definition with examples, synonyms, and related words.

Accessing Route Parameters

In any page component, access params via Astro.params:
---
const { lang, slug } = Astro.params;

// Use params to fetch data
const collectionName = lang === "es" ? "words-es" : "words-en";
const entry = await getEntry(collectionName, slug);
---

URL Construction

Build URLs dynamically based on language:
---
const { lang } = Astro.params;
const prefix = lang === "en" ? "/en" : "/es";
---

<nav>
  <a href={`${prefix}/`}>Home</a>
  <a href={`${prefix}/buscar/`}>Search</a>
  <a href={`${prefix}/indice/`}>Index</a>
</nav>

Prerendering Strategy

Most pages use prerender: true for static site generation:
---
export const prerender = true;
---
Benefits:
  • Faster page loads
  • Better SEO
  • Lower hosting costs

i18n Configuration

Astro’s built-in i18n is configured in astro.config.mjs:
export default defineConfig({
  i18n: {
    defaultLocale: "es",
    locales: ["es", "en"],
    routing: {
      prefixDefaultLocale: true,  // Forces /es/ and /en/ prefixes
      redirectToDefaultLocale: false,  // Allows custom redirect logic
    },
  },
});

Hreflang for SEO

Every page includes alternate language URLs for SEO:
---
const alternateUrls = {
  es: "/es/",
  en: "/en/",
  xDefault: "/es/",  // Default fallback
};
---

<Base alternateUrls={alternateUrls}>
  <!-- Content -->
</Base>
The Base layout renders these as <link rel="alternate"> tags:
<link rel="alternate" hreflang="es" href="https://chapinismos.org/es/" />
<link rel="alternate" hreflang="en" href="https://chapinismos.org/en/" />
<link rel="alternate" hreflang="x-default" href="https://chapinismos.org/es/" />

Best Practices

const validLangs = ["es", "en"];
if (!validLangs.includes(lang)) {
  return Astro.redirect("/es/");
}
import { useTranslations } from "../../utils/i18n";
const t = useTranslations(lang);

<h1>{t("home.title")}</h1>
Always use the language prefix:
href={`/${lang}/palabras/${slug}/`}
Astro config uses trailingSlash: "always" for consistency:
/es/buscar/  ✓
/es/buscar   ✗

Next Steps

Components

Learn about reusable components

Layouts

Understand page layouts

Content Collections

Work with word data

Internationalization

Deep dive into i18n

Build docs developers (and LLMs) love