# Astro content collections: schemas, types and queries

A collection is a Zod schema plus a loader: Astro validates files at build time, generates the types and hands you getCollection.

> Page: https://ui.eneawork.it/blog/astro-content-collections
> Markdown: https://ui.eneawork.it/blog/astro-content-collections.md

*2026-11-01*

Una content collection in Astro è una cartella di file più uno schema Zod: il loader legge i file, lo schema li valida alla build e getCollection() restituisce oggetti tipizzati. Niente frontmatter lasciato al caso.

Il pacchetto astro ha 10.765.753 download settimanali nella settimana 23-29 ottobre 2026, letto da api.npmjs.org il 1 novembre 2026. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. Ogni comando qui sotto gira in locale, senza account.

## Un file di configurazione, non una cartella magica

// src/content.config.ts import { defineCollection, z } from "astro:content"; import { glob } from "astro/loaders"; const blog = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }), schema: z.object({ title: z.string(), publishedAt: z.coerce.date(), draft: z.boolean().default(false), tags: z.array(z.string()).default([]), }), }); export const collections = { blog };

Il file va in src/content.config.ts. Astro 4 usava src/content/config.ts: la posizione nuova serve perché una collection può leggere file anche fuori da src/content.

## Leggere una collection in una pagina

--- import { getCollection, render } from "astro:content"; const posts = (await getCollection("blog", ({ data }) => !data.draft)) .sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf()); --- {posts.map(async (post) => { const { Content } = await render(post); return {post.data.title}; })}

Il secondo argomento di getCollection è un filtro in memoria, non una query: la collection resta tipizzata e il filtro riceve già i dati validati. Per un solo elemento usa getEntry("blog", id).

## Una route dinamica per ogni entry

--- import type { GetStaticPaths } from "astro"; import { getCollection, render } from "astro:content"; export const getStaticPaths = (async () => { const posts = await getCollection("blog", ({ data }) => !data.draft); return posts.map((post) => ({ params: { id: post.id }, props: { post } })); }) satisfies GetStaticPaths; const { post } = Astro.props; const { Content } = await render(post); ---

Il file [id].astro chiama getStaticPaths e restituisce una entry per ogni post pubblicato: la build scrive una pagina per ciascuna, senza query a runtime. Una collection da 638 file produce 638 pagine, e ogni pagina riceve l’entry già validata in Astro.props.

## Dove metti i vincoli

La validazione avviene quando il loader legge i file: build e dev server. Non è un controllo a runtime, quindi una risposta presa da una API non passa di qui.

Misurato il 1 novembre 2026 sul repository EneUI: data/blog contiene 638 file JSON per 8,32 MB, con 10.285 blocchi di corpo, 903 blocchi code e 8 tipi di blocco (p, h2, h3, ul, ol, table, callout, code). Metodo: readdirSync più JSON.parse su ogni file, con uno script Node eseguito in locale.

node -e "const fs=require('fs');const d='data/blog';let n=0,b=0;for(const f of fs.readdirSync(d)){if(!f.endsWith('.json')||f[0]==='.')continue;n++;b+=JSON.parse(fs.readFileSync(d+'/'+f,'utf8')).body.length;}console.log(n+' files, '+(b/n).toFixed(2)+' blocks each')"

## Gli errori che vedi davvero

## La regola di decisione

Regola: se il contenuto vive nel repository e cambia con un commit, glob() è la scelta; se cambia senza deploy, come un prezzo o uno stock, serve un loader che legga da una API e la collection resta il posto dello schema. Il segnale è la frequenza: più aggiornamenti al giorno che deploy al giorno sposta il confine.

Lo schema cresce con il contenuto. Quando i campi superano la decina, estrai gli oggetti condivisi, seo, cover, author, in variabili Zod e riusali fra collection: una modifica al campo alt tocca un punto solo invece di quattro.

## Il limite noto

Una collection non è un CMS. Il loader glob() legge file dentro il repository, quindi ogni modifica di contenuto richiede un deploy. Per contenuti che cambiano ogni ora serve un loader remoto o una route API; la collection resta il posto dello schema, non della scrittura.

## Comandi

npx astro sync npx astro check

Dal catalogo: i componenti Astro Association, il blocco logo Astro e glass cards. Le guide: la direttiva client mancante e view transitions in Astro; il confronto: Astro contro Next.js sulla documentazione.

## FAQ

### Dove va il file di configurazione in Astro 5?

In src/content.config.ts. Astro 4 usava src/content/config.ts: quel percorso funziona ancora, ma il nuovo è quello documentato.

### Posso usare le collection senza Zod?

Sì, lo schema è facoltativo. Senza schema perdi la validazione e i tipi generati, quindi tieni almeno i campi che usi nel template.

### Le collection funzionano con MDX?

Sì. Il pattern del loader glob deve includere mdx, altrimenti quei file restano fuori dalla collection.

### Come filtro gli articoli non pubblicati?

Aggiungi un campo booleano allo schema, per esempio draft con valore predefinito false, e filtra dentro getCollection.

### Meglio una collection o una route API?

Una collection per file versionati nel repository, una route API per contenuti che cambiano senza deploy. Lo schema serve in entrambi i casi.
