03. Routing, pagine ed endpoint

← 02 - Progetto, componenti e stili · Indice · successivo → 04 - Contenuti - Markdown, MDX e content collections

3.1 File-based routing

Nessun file di configurazione delle rotte. Il filesystem è la configurazione.

src/pages/index.astro          →  /
src/pages/chi-sono.astro       →  /chi-sono
src/pages/guida/index.astro    →  /guida
src/pages/guida/setup.md       →  /guida/setup
src/pages/rss.xml.ts           →  /rss.xml

Estensioni valide in src/pages/: .astro, .md, .mdx (con l’integrazione), .html, .js/.ts (endpoint).

Escludere un file dal routing: prefisso _. src/pages/_bozza.astro e tutta la cartella src/pages/_parti/ non generano rotte né finiscono in dist/. Comodo per tenere componenti accanto alle pagine che li usano.

Pagine speciali:

  • src/pages/404.astro → genera 404.html.
  • src/pages/500.astro → solo per rotte on-demand. Riceve una prop error. Attenzione a non stampare lo stack in produzione.
---
// src/pages/500.astro
interface Props { error: unknown }
const { error } = Astro.props;
---
<p>{import.meta.env.DEV && error instanceof Error ? error.message : 'Errore interno'}</p>

📖 Routing

3.2 Rotte dinamiche

src/pages/blog/[slug].astro        →  /blog/qualcosa
src/pages/[lang]/[versione].astro  →  /it/v1
src/pages/docs/[...path].astro     →  /docs/a, /docs/a/b/c, e anche /docs

[...rest] cattura più segmenti, / inclusi, e può anche essere vuoto.

Quando serve [...rest] e non [slug]: quando gli identificatori contengono uno slash. È esattamente il caso delle content collections con file in sottocartelle (avanzato/annidato è un id valido). Se usi [id] semplice, le pagine annidate semplicemente non vengono generate e ti chiedi perché mancano.

Ordine di priorità

Quando due pattern potrebbero matchare lo stesso URL, vince in ordine:

  1. rotte riservate di Astro (_astro/, _actions/, _server_islands/)
  2. rotte con più segmenti
  3. rotte statiche > rotte dinamiche
  4. parametro nominato [x] > rest [...x]
  5. rotte dinamiche prerenderizzate > rotte dinamiche on-demand
  6. endpoint > pagine
  7. rotte da file > redirect

Quindi pages/posts/crea.astro vince su pages/posts/[id].astro per /posts/crea.

📖 Routing reference

3.3 getStaticPaths()

Su una rotta dinamica prerenderizzata, Astro deve sapere a build time quali URL generare. Glielo dici con getStaticPaths().

---
// src/pages/blog/[slug].astro
import type { GetStaticPaths } from 'astro';
 
export const getStaticPaths: GetStaticPaths = async () => {
  const risposta = await fetch('https://api.example.com/post');
  const post = await risposta.json();
 
  return post.map((p) => ({
    params: { slug: p.slug },   // finisce nell'URL → solo stringhe
    props: { post: p },         // NON finisce nell'URL → qualunque tipo
  }));
};
 
const { slug } = Astro.params;
const { post } = Astro.props;
---
<h1>{post.titolo}</h1>

Regole che contano:

  • params accetta solo stringhe (o undefined per un rest vuoto). Dalla v6 passare un numero è un errore, non più una conversione silenziosa.
  • props è il canale per i dati: usalo, così non rifai il fetch nel template.
  • La funzione gira in uno scope isolato: può usare solo gli import del file, non variabili definite altrove nel frontmatter.
  • L’oggetto Astro è deprecato qui dentro (dalla v6): usa import.meta.env.SITE al posto di Astro.site.
  • I params non vengono più decodificati automaticamente (dalla v5): se hai caratteri percent-encoded, decodeURI() a mano.

Su una rotta on-demand getStaticPaths() non va usata: la rotta risponde a qualunque URL che matcha il pattern, e i dati li recuperi nel frontmatter.

---
export const prerender = false;
const { slug } = Astro.params;
const post = await trovaPost(slug);
if (!post) return Astro.redirect('/404');
---

3.4 Paginazione

paginate() arriva come argomento delle opzioni di getStaticPaths().

---
// src/pages/articoli/[page].astro   ✅ verificato su Astro 7.2.3
import type { GetStaticPathsOptions } from 'astro';
import { getCollection } from 'astro:content';
 
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
  const articoli = await getCollection('blog');
  return paginate(articoli, { pageSize: 10 });
}
 
const { page } = Astro.props;
---
<ul>
  {page.data.map((a) => <li><a href={`/blog/${a.id}/`}>{a.data.title}</a></li>)}
</ul>
 
<nav>
  {page.url.first && <a href={page.url.first}>« Prima</a>}
  {page.url.prev  && <a href={page.url.prev}>‹ Precedente</a>}
  <span>{page.currentPage} / {page.lastPage}</span>
  {page.url.next  && <a href={page.url.next}>Successiva ›</a>}
  {page.url.last  && <a href={page.url.last}>Ultima »</a>}
</nav>

Nome file: [page].astro genera /articoli/1, /articoli/2, … [...page].astro genera /articoli, /articoli/2, … (la prima pagina senza numero: di solito è quello che vuoi).

L’oggetto page contiene: data, start, end, total, currentPage, size, lastPage, url.{current,prev,next,first,last}.

Cambiato da Astro 4

page.url.* include già il base configurato. Se in un vecchio progetto prefissavi import.meta.env.BASE_URL a mano, ora ottieni un doppio prefisso.

Novità 7.1: format per personalizzare gli URL generati:

return paginate(dati, { pageSize: 10, format: (url) => `${url}.html` });

Serve per gli host che servono solo URL con estensione. Non è documentato se la trasformazione si propaghi anche a page.url.prev/next: se ti serve, verifica in un progetto reale prima di appoggiarti al comportamento.

Paginazione annidata (per tag, per categoria): restituisci un array di paginate(), uno per gruppo, passando params:

---
export const getStaticPaths = (({ paginate }) => {
  const tags = ['astro', 'css', 'note'];
  return tags.flatMap((tag) =>
    paginate(postConTag(tag), { params: { tag }, pageSize: 10 })
  );
}) satisfies GetStaticPaths;
---

Con src/pages/[tag]/[page].astro ottieni /astro/1, /css/1, …

3.5 Endpoint

Un file .ts/.js in src/pages/ che esporta funzioni con il nome del metodo HTTP. L’estensione del file finale va nel nome: data.json.ts → /data.json.

// src/pages/api/ricerca.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
 
export const GET: APIRoute = async () => {
  const pagine = await getCollection('guide');
  const indice = pagine.map((p) => ({
    id: p.id,
    titolo: p.data.title,
    testo: p.body?.slice(0, 500) ?? '',
  }));
 
  return new Response(JSON.stringify(indice), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Questo è, per esempio, il modo di costruirsi un indice di ricerca statico per una guida: generi il JSON a build time e lo interroghi lato client con Fuse.js o MiniSearch. Nessun server, nessun servizio esterno.

Altri metodi: POST, PUT, DELETE, e ALL come catch-all. Se esporti GET ma non HEAD, Astro gestisce HEAD da solo.

Gli endpoint sono prerenderizzati per default come tutto il resto: per farli rispondere a runtime serve export const prerender = false e un adapter.

Cambiato in Astro 6

Un endpoint il cui nome finisce con un’estensione (/sitemap.xml) è raggiungibile solo senza trailing slash, qualunque sia la config trailingSlash.

📖 Endpoints

3.6 Redirect e rewrite

Redirect statici, in configurazione:

export default defineConfig({
  redirects: {
    '/vecchio': '/nuovo',
    '/blog/[...slug]': '/articoli/[...slug]',
    '/altrove': { status: 302, destination: 'https://example.com' },
  },
});

In build statico senza adapter Astro emette un HTML con <meta http-equiv="refresh"> (gli status code non si applicano). Con un adapter scrive il file di configurazione redirect dell’host, che è molto meglio.

Redirect a runtime: return Astro.redirect('/login'), deve essere ritornato da una pagina, non da un componente figlio (a quel punto gli header sono già partiti).

Rewrite: serve il contenuto di un’altra rotta senza cambiare l’URL.

---
const { articolo } = Astro.params;
if (!esiste(articolo)) return Astro.rewrite('/404');
---

Differenza da tenere a mente: rewrite() fa ripartire un ciclo di rendering completo (rieseguendo il middleware); next(url) dentro il middleware riscrive in loco senza rieseguirlo.

3.7 Internazionalizzazione

export default defineConfig({
  i18n: {
    locales: ['it', 'en'],
    defaultLocale: 'it',
    routing: {
      prefixDefaultLocale: false,   // /chi-sono e /en/chi-sono
    },
    fallback: { en: 'it' },
  },
});

Le cartelle in src/pages/ devono chiamarsi esattamente come i locale. Con prefixDefaultLocale: false la lingua di default sta nella root (src/pages/chi-sono.astro) e le altre in sottocartelle (src/pages/en/chi-sono.astro).

Helper da astro:i18n: getRelativeLocaleUrl(), getAbsoluteLocaleUrl(), getRelativeLocaleUrlList(), getPathByLocale(), getLocaleByPath().

---
import { getRelativeLocaleUrl } from 'astro:i18n';
---
<a href={getRelativeLocaleUrl('en', 'chi-sono')}>English</a>

Utili nel render context: Astro.currentLocale (funziona anche in statico), Astro.preferredLocale e Astro.preferredLocaleList (solo on-demand, leggono Accept-Language).

Cambiato in Astro 6

i18n.routing.redirectToDefaultLocale ha default false (prima era true) e si può usare solo con prefixDefaultLocale: true. La combinazione vecchia poteva generare loop di redirect infiniti.

📖 Internationalization

3.8 L’oggetto Astro. le proprietà che userai davvero

ProprietàA cosa serve
Astro.propsle props del componente / da getStaticPaths()
Astro.paramsi segmenti dinamici dell’URL
Astro.urlURL della richiesta corrente
Astro.sitel’URL dal campo site della config
Astro.redirect() / Astro.rewrite()vedi 3.6
Astro.currentLocalelingua dedotta dall’URL
Astro.routePatternil pattern della rotta, es. /blog/[slug]
Astro.isPrerenderedutile nel middleware
Astro.cookies, Astro.request, Astro.response, Astro.sessionsolo on-demand → modulo 6

📖 API reference

Esercizi

  1. Crea src/pages/guida/[...id].astro e genera tre pagine, di cui una in una sottocartella. Poi cambia [...id] in [id] e osserva quale sparisce: è la dimostrazione pratica del punto 3.2.
  2. Aggiungi la paginazione a una lista di 25 elementi finti, con pageSize: 10 e navigazione prima/precedente/successiva/ultima.
  3. Scrivi un endpoint /indice.json che espone titolo e id di tutte le tue pagine. Aprilo nel browser dopo astro build && astro preview.
  4. Aggiungi una 404 personalizzata e verifica che astro preview la serva.