Changer le thème

Guide complet Astro Content Collections : du concept à la validation Schema

Easton editorial illustration: performance inspection lens

La page d’accueil du blog plante : un article a un champ publishDate mal formaté. J’ai passé une demi-heure à fouiller les fichiers — la date était 2024/12/01 au lieu de 2024-12-01. Et ce n’est qu’un petit blog de 30 articles ; avec des centaines d’articles, chaque nouveau champ impliquerait une vérification manuelle.

Les Content Collections règlent ce problème : Astro détecte les erreurs de contenu comme TypeScript détecte les erreurs de code. Une fois le Schema configuré, l’éditeur propose l’autocomplétion — plus besoin de chercher les noms de champs dans la doc. Cet article explique ce que sont les Content Collections, comment écrire la configuration et utiliser la validation Schema.

Que sont les Content Collections ? Pourquoi en avoir besoin ?

Vous pourriez penser : les Content Collections, ce n’est pas juste gérer des dossiers Markdown ? Pourquoi ne pas créer un dossier blog/ sous src/pages/ ?

Oui, fonctionnellement c’est possible. Mais cette approche n’offre aucune sécurité de typage.

Avec l’approche traditionnelle, votre frontmatter ressemble à ceci :


---

title: "Titre de mon blog"
date: "2024-12-01"
tags: ["Astro", "tutoriel"]

---

Contenu de l'article...

Rien d’anormal en apparence. Mais imaginez :

  • Vous écrivez tag au lieu de tags (sans le s)
  • La date est 12/01/2024 au lieu de 2024-12-01
  • Vous ajoutez un champ author mais oubliez de le mettre sur d’anciens articles

Ces erreurs, Astro ne les signale pas à l’avance. Ce n’est qu’au runtime, quand la page plante, que vous découvrez le problème.

Les Content Collections existent pour ça. C’est un système de gestion de contenu typé — en bref : ajouter la vérification TypeScript aux fichiers Markdown.

Concrètement, les Content Collections offrent :

  1. Validation Schema : types et structure du frontmatter ; non-conformité = erreur immédiate
  2. Génération automatique de types : TypeScript basé sur le Schema, autocomplétion dans l’éditeur
  3. API de requête unifiée : getCollection() et autres méthodes retournent des données typées
  4. Optimisation des performances : la Content Layer API d’Astro 5.0 accélère les requêtes

En résumé : l’approche traditionnelle est « libre mais risquée », les Content Collections sont « contraintes mais fiables ». Un peu de temps de configuration évite 99 % des erreurs bêtes.

Franchement, tous mes projets Astro utilisent les Content Collections. Une configuration, des bénéfices sur tout le projet.

Configuration pratique des Content Collections

Théorie terminée, passons à la pratique. Trois étapes : créer les répertoires, écrire la config, créer le contenu.

Étape 1 : créer les répertoires

Les Content Collections exigent que le contenu soit dans src/content/. C’est un répertoire réservé Astro (depuis v2.0), dédié aux collections de contenu.

Structure typique :

src/
├── content/
│   ├── blog/          # Collection blog
│   │   ├── post-1.md
│   │   └── post-2.md
│   └── docs/          # Collection docs
│       ├── guide-1.md
│       └── guide-2.md
├── content.config.ts   # Fichier de config (attention à l'emplacement)
└── pages/
    └── ...

Attention : le fichier de config est src/content.config.ts (ou .js, .mjs), pas dans content/. Au début, je l’avais mis au mauvais endroit — j’ai perdu du temps à chercher.

Chaque sous-répertoire est une collection. src/content/blog/ = collection blog, src/content/docs/ = collection docs.

Étape 2 : écrire le fichier de configuration

Créez src/content.config.ts, le cœur des Content Collections :

// src/content.config.ts
import { defineCollection, z } from 'astro:content';

// Définir la collection blog
const blogCollection = defineCollection({
  type: 'content',  // Type : content = fichiers Markdown/MDX
  schema: z.object({
    title: z.string(),                    // Titre (obligatoire)
    description: z.string(),              // Description (obligatoire)
    pubDate: z.coerce.date(),             // Date de publication (convertie en Date)
    tags: z.array(z.string()).optional(), // Tableau de tags (optionnel)
    draft: z.boolean().default(false),    // Brouillon (false par défaut)
  }),
});

// Exporter l'objet collections
export const collections = {
  'blog': blogCollection,  // La clé correspond au nom du répertoire
};

Décomposons :

  1. defineCollection() : définit la configuration d’une collection
  2. type: 'content' : collection de fichiers Markdown/MDX
  3. schema : structure du frontmatter définie avec Zod (bibliothèque de validation)
  4. Objet collections : export des collections ; la clé doit correspondre au nom du répertoire

La partie schema est cruciale. Chaque champ utilise z.xxx() :

  • z.string() : chaîne
  • z.coerce.date() : convertit automatiquement une chaîne en Date
  • z.array(z.string()) : tableau de chaînes
  • .optional() : champ optionnel
  • .default(false) : valeur par défaut

Étape 3 : créer les fichiers de contenu

Une fois configuré, créez des fichiers Markdown dans src/content/blog/ :


---

title: "Introduction aux Content Collections Astro"
description: "Apprenez à configurer et utiliser les Content Collections"
pubDate: "2024-12-01"
tags: ["Astro", "tutoriel"]

---

Contenu de l'article...

Si le frontmatter respecte le Schema, Astro parse normalement. Sinon (ex. pubDate mal formaté), Astro signale l’erreur à la compilation.

Interroger les données dans les pages

Vous pouvez ensuite interroger le contenu dans n’importe quel fichier Astro :


---

// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

// Récupérer tous les articles
const allPosts = await getCollection('blog');

// Filtrer les brouillons (draft: true)
const publishedPosts = allPosts.filter(post => !post.data.draft);

---

<ul>
  {publishedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>
        {post.data.title}
      </a>
      <p>{post.data.description}</p>
    </li>
  ))}
</ul>

post.data contient le frontmatter — avec l’autocomplétion TypeScript complète. Dans VS Code, tapez post.data. et l’éditeur suggère title, description, pubDate, etc.

C’est l’intérêt des Content Collections : typage + autocomplétion — une expérience de développement nettement meilleure.

Validation Schema en profondeur

La section précédente couvrait z.string(), z.coerce.date() et les types de base. Le Schema va bien plus loin. Voyons les usages Zod avancés.

Types de base — aide-mémoire

import { z } from 'astro:content';

z.string()           // Chaîne
z.number()           // Nombre
z.boolean()          // Booléen
z.date()             // Objet Date
z.coerce.date()      // Convertit automatiquement une chaîne en Date
z.array(z.string())  // Tableau de chaînes
z.enum(['draft', 'published'])  // Énumération (valeurs fixes)

z.coerce.date() est particulièrement utile. En frontmatter Markdown, les dates sont des chaînes ("2024-12-01"). Avec z.date(), Astro exige un objet Date et échoue. z.coerce.date() convertit automatiquement — gain de temps évident.

Champs optionnels et valeurs par défaut

Tous les champs ne sont pas obligatoires. Pour tags, par exemple :

schema: z.object({
  title: z.string(),                    // Obligatoire
  tags: z.array(z.string()).optional(), // Optionnel
  draft: z.boolean().default(false),    // Valeur par défaut
})

.default() remplit automatiquement la valeur si le champ est absent du frontmatter.

Usage avancé : validation d’images

Astro fournit le type image() pour valider les chemins d’images :

import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  schema: ({ image }) => z.object({  // Note : forme fonction
    title: z.string(),
    cover: image(),  // Validation du chemin d'image
  }),
});

image() vérifie que le chemin pointe vers un fichier image valide (chemins relatifs supportés). Pratique pour afficher des couvertures sur la page d’accueil du blog.

Référencer d’autres collections : z.reference()

Parfois le contenu est lié. Un article appartient à une catégorie qui est elle-même une collection :

// Collection catégories
const categoryCollection = defineCollection({
  schema: z.object({
    name: z.string(),
    slug: z.string(),
  }),
});

// Collection blog référençant les catégories
const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    category: z.reference('category'),  // Référence la collection category
  }),
});

export const collections = {
  'category': categoryCollection,
  'blog': blogCollection,
};

Dans le frontmatter de l’article, category contient le nom de fichier de la catégorie (sans extension) :


---

title: "Mon article"
category: "tech"  # Référence src/content/category/tech.md

---

Astro vérifie que la catégorie existe — typage garanti.

Objets imbriqués complexes

Pour un frontmatter structuré :

schema: z.object({
  title: z.string(),
  author: z.object({
    name: z.string(),
    email: z.string().email(),  // Validation email
    avatar: z.string().url(),   // Validation URL
  }),
  seo: z.object({
    keywords: z.array(z.string()),
    description: z.string().max(160),  // Longueur max
  }).optional(),
})

Frontmatter correspondant :


---

title: "Titre de l'article"
author:
  name: "Jean Dupont"
  email: "[email protected]"
  avatar: "https://example.com/avatar.jpg"
seo:
  keywords: ["Astro", "tutoriel"]
  description: "Tutoriel sur Astro"

---

La magie du typage : inférence TypeScript automatique

Après configuration du Schema, Astro génère les types TypeScript. L’autocomplétion est complète :

import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

posts.forEach(post => {
  // L'éditeur suggère tous les champs de post.data
  console.log(post.data.title);       // ✅ Type : string
  console.log(post.data.pubDate);     // ✅ Type : Date
  console.log(post.data.tags);        // ✅ Type : string[] | undefined
  console.log(post.data.notExist);    // ❌ Erreur de compilation : champ inexistant
});

Pas besoin d’écrire les types manuellement — Astro les génère à partir du Schema, avec précision.

getEntry() vs getCollection()

Différence entre les API de requête :

  • getCollection('blog') : tout le contenu d’une collection
  • getEntry('blog', 'my-post') : un article par slug

Pour une page détail, getEntry() est plus efficace :


---

// src/pages/blog/[slug].astro
import { getEntry } from 'astro:content';

const { slug } = Astro.params;
const post = await getEntry('blog', slug);

if (!post) {
  return Astro.redirect('/404');
}

const { Content } = await post.render();

---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Au début, la syntaxe Zod m’a un peu déconcerté. Après quelques usages, ça devient naturel — et les messages d’erreur Zod sont clairs pour le dépannage.

Problèmes courants et solutions

Configurer les Content Collections, c’est aussi rencontrer des erreurs. Voici les plus fréquentes — celles que j’ai moi-même rencontrées.

Erreur 1 : MarkdownContentSchemaValidationError

L’erreur la plus courante : le frontmatter ne respecte pas le Schema. Message typique :

blog → my-post.md frontmatter does not match collection schema.
- "title" is required
- "pubDate" must be a valid date

Comment lire cette erreur ?

Astro indique clairement le fichier (my-post.md) et les champs en cause (title, pubDate).

Causes et solutions :

  1. Champ manquant : un champ obligatoire du Schema est absent du frontmatter

    • Solution : compléter le champ, ou ajouter .optional() dans le Schema
  2. Faute de frappe : pubDate écrit publishDate

    • Solution : uniformiser les noms ; utilisez l’autocomplétion de l’éditeur
  3. Type incorrect : le Schema attend z.number() mais le frontmatter contient une chaîne

    • Solution : vérifier le format de la valeur

Erreur 2 : InvalidContentEntryFrontmatterError

Le frontmatter lui-même est invalide (erreur de syntaxe YAML) — même le parsing échoue.

Exemple :


---

title: "Mon titre
description: "Guillemet non fermé"

---

Solution : vérifier la syntaxe YAML — guillemets, deux-points, indentation. Un plugin de validation YAML dans l’éditeur aide beaucoup.

Erreur 3 : problèmes de format de date

Piège classique. Avec z.date() au lieu de z.coerce.date(), Astro exige un objet Date — impossible en YAML qui n’accepte que des chaînes !

Solution : utilisez z.coerce.date() dans le Schema :

// ❌ Incorrect : exige un Date, le frontmatter contient une chaîne
pubDate: z.date()

// ✅ Correct : convertit automatiquement la chaîne en Date
pubDate: z.coerce.date()

Données héritées : .passthrough()

Si votre blog a beaucoup d’anciens articles avec des frontmatters hétérogènes, .passthrough() assouplit temporairement la validation :

schema: z.object({
  title: z.string(),
  // ... autres champs
}).passthrough()  // Autorise des champs supplémentaires non définis

C’est une solution provisoire. À long terme, uniformisez la structure du frontmatter.

Multi-collections : comment organiser

Pour un site avec blog, docs et études de cas :

src/content/
├── blog/
├── docs/
└── case-studies/

Dans content.config.ts, définissez chaque collection :

const blogCollection = defineCollection({ /* ... */ });
const docsCollection = defineCollection({ /* ... */ });
const caseStudiesCollection = defineCollection({ /* ... */ });

export const collections = {
  'blog': blogCollection,
  'docs': docsCollection,
  'case-studies': caseStudiesCollection,
};

Chaque collection peut avoir un Schema différent — sans interférence.

Bonnes pratiques de conception Schema

Mon expérience en résumé :

  1. Peu de champs obligatoires : seulement l’essentiel ; le reste en .optional() ou .default()
  2. Dates avec z.coerce.date() : évite les conversions manuelles
  3. Noms en camelCase : pubDate plutôt que pub_date (convention JavaScript)
  4. Objets complexes : plusieurs collections liées par z.reference()
  5. Commentaires clairs : documentez chaque champ dans le Schema pour l’équipe

Checklist de dépannage

En cas d’erreur, vérifiez dans cet ordre :

  • Le répertoire src/content/ existe-t-il ?
  • src/content.config.ts est-il au bon emplacement ? (pas dans content/)
  • Les clés de l’objet collections correspondent-elles aux noms de répertoires ?
  • La syntaxe YAML du frontmatter est-elle correcte ? (guillemets, deux-points, indentation)
  • Tous les champs obligatoires sont-ils remplis ?
  • Les types correspondent-ils au Schema ?

Franchement, ça paraît complexe, mais les messages d’erreur Astro sont déjà très explicites. Lisez-les attentivement — le problème se localise en général rapidement.

Conclusion

Revenons aux trois questions du début :

Vous ne saviez pas ce que sont les Content Collections ? C’est la vérification TypeScript appliquée au contenu Markdown. Astro détecte les erreurs à la compilation, pas quand la page plante.

Comment écrire la configuration ? Trois étapes : créer src/content/, écrire src/content.config.ts, définir le Schema avec defineCollection() et Zod. Les clés doivent correspondre aux noms de répertoires.

Comment utiliser la validation Schema ? Maîtrisez les types de base (z.string(), z.coerce.date(), z.array()), .optional() et .default(), et lisez les messages d’erreur Astro.

Les Content Collections sont, à mon avis, l’une des fonctionnalités les plus utiles d’Astro. Un peu de configuration au départ, des heures de débogage évitées ensuite. Et l’autocomplétion dans l’éditeur change vraiment l’expérience de développement.

Prochaines étapes

Pour essayer dès maintenant :

  1. Nouveau projet : configurez les Content Collections dès le départ — établissez les conventions dès le début
  2. Projet existant : utilisez .passthrough() pour faire tourner le contenu actuel, puis uniformisez progressivement le frontmatter
  3. Documentation officielle : en cas de doute, consultez la documentation Astro — référence API complète

Les Content Collections ne sont pas difficiles — il faut surtout pratiquer. Mille tutoriels ne valent pas une configuration écrite de vos propres mains. Essayez — vous apprécierez cette sensation de typage sûr.

Configuration complète d'Astro Content Collections

Étapes complètes de la configuration des Content Collections à la validation Schema pour un système de contenu typé

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Créer la structure de répertoires

    Créez le répertoire src/content/ à la racine du projet :
    • Répertoire réservé Astro (depuis v2.0)
    • Créez des sous-répertoires dans content/ comme collections (ex. blog/, docs/)
    • Chaque sous-répertoire est une collection

    Note : le fichier de config src/content.config.ts n'est pas dans content/, mais dans src/.
  2. 2

    Step 2: Créer le fichier de configuration

    Créez le fichier src/content.config.ts :

    1. Importez les dépendances :
    import { defineCollection, z } from 'astro:content'

    2. Définissez la configuration de collection :
    const blogCollection = defineCollection({
    type: 'content',
    schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).optional(),
    draft: z.boolean().default(false)
    })
    })

    3. Exportez l'objet collections :
    export const collections = { 'blog': blogCollection }
    Note : la clé doit correspondre au nom du répertoire
  3. 3

    Step 3: Créer les fichiers de contenu

    Créez des fichiers Markdown dans src/content/blog/ :

    Le frontmatter doit respecter le Schema :
    • title (chaîne obligatoire)
    • description (chaîne obligatoire)
    • pubDate (format date comme "2024-12-01", z.coerce.date() convertit automatiquement)
    • tags (tableau de chaînes optionnel)
    • draft (booléen optionnel, false par défaut)

    Si un champ ne respecte pas le Schema, Astro signale l'erreur à la compilation.
  4. 4

    Step 4: Interroger les données dans les pages

    Importez getCollection dans un fichier Astro :
    import { getCollection } from 'astro:content'

    Récupérer tous les articles :
    const allPosts = await getCollection('blog')

    Filtrer les brouillons :
    const publishedPosts = allPosts.filter(post => !post.data.draft)

    Utilisation :
    • post.data a l'autocomplétion TypeScript complète
    • L'éditeur suggère title, description, pubDate, etc.
    • Pour un seul article, getEntry('blog', slug) est plus efficace
  5. 5

    Step 5: Configuration Schema avancée

    Validation d'images :
    schema: ({ image }) => z.object({
    cover: image()
    })

    Référencer d'autres collections :
    z.reference('category')

    Objets imbriqués complexes :
    z.object({
    author: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url()
    })
    })

    Données héritées :
    .passthrough() autorise des champs supplémentaires non définis

    Multi-collections :
    définissez blog, docs, case-studies, etc. séparément dans l'objet collections
  6. 6

    Step 6: Dépannage des erreurs courantes

    MarkdownContentSchemaValidationError : champs manquants (compléter ou ajouter .optional()), fautes de frappe (uniformiser les noms), types incorrects (vérifier le format). InvalidContentEntryFrontmatterError : syntaxe YAML (guillemets, deux-points, indentation). Dates : utilisez z.coerce.date() plutôt que z.date(). Checklist : répertoire content/ existe, content.config.ts au bon emplacement, clés collections = noms de répertoires, YAML correct, champs obligatoires remplis, types conformes au Schema.

FAQ

Que sont les Content Collections ? Pourquoi en avoir besoin ?
Les Content Collections sont un système de gestion de contenu typé : en essence, elles ajoutent la vérification TypeScript aux fichiers Markdown.

L'approche traditionnelle (créer un dossier blog/ directement sous src/pages/) n'offre aucune sécurité de typage. Risques fréquents :
• Fautes de frappe (tags écrit tag)
• Format de date incorrect (12/01/2024 au lieu de 2024-12-01)
• Nouveau champ oublié sur d'anciens articles
• Astro ne prévient pas — l'erreur n'apparaît qu'au runtime quand la page plante

Les Content Collections apportent :
1) Validation Schema (types et structure du frontmatter, erreur immédiate si non conforme)
2) Génération automatique de types (TypeScript basé sur le Schema, autocomplétion)
3) API de requête unifiée (getCollection() retourne des données typées)
4) Optimisation des performances (Content Layer API d'Astro 5.0 accélère les requêtes)

L'approche traditionnelle est « libre mais risquée » ; les Content Collections sont « contraintes mais fiables ». Une configuration initiale profite à tout le projet.
Comment configurer les Content Collections ? Quel est le flux complet ?
Trois étapes :

1) Créer les répertoires :
• src/content/ (répertoire réservé Astro depuis v2.0)
• Sous-répertoires comme collections (blog/, docs/) — chaque sous-répertoire = une collection

2) Écrire la configuration :
• src/content.config.ts (pas dans content/)
• Importer defineCollection et z
• Définir la collection (type: 'content' = Markdown/MDX, schema Zod pour le frontmatter)
• Exporter collections (clé = nom du répertoire, ex. 'blog': blogCollection)

3) Créer le contenu :
• Fichiers Markdown dans src/content/blog/
• Frontmatter conforme au Schema — sinon erreur à la compilation
Comment utiliser la validation Schema ? Quels types courants ?
Définir les types avec Zod :
• z.string() chaîne
• z.number() nombre
• z.boolean() booléen
• z.date() objet Date
• z.coerce.date() convertit automatiquement une chaîne en Date (très pratique — YAML n'accepte que des chaînes)
• z.array(z.string()) tableau de chaînes
• z.enum(['draft', 'published']) énumération

Champs optionnels et valeurs par défaut :
• .optional() champ optionnel
• .default(false) valeur par défaut

Usages avancés :
• image() pour valider un chemin d'image : schema: ({ image }) => z.object({ cover: image() })
• z.reference('category') pour référencer une autre collection
• Objets imbriqués : z.object({ author: z.object({ name: z.string(), email: z.string().email() }) })

Après configuration, Astro génère les types TypeScript — post.data est entièrement typé.
Comment interroger les Content Collections ? Différence entre getCollection et getEntry ?
API de requête :
• getCollection('blog') — tout le contenu d'une collection, retourne un tableau
• getEntry('blog', 'my-post') — un article par slug, retourne un objet (plus efficace pour une page détail)

Import dans un fichier Astro :
import { getCollection, getEntry } from 'astro:content'

Utilisation :
• post.data = données frontmatter avec autocomplétion TypeScript
• L'éditeur suggère title, description, pubDate, etc.

Filtrer les brouillons :
const publishedPosts = allPosts.filter(post => !post.data.draft)

Exemple article unique :
const post = await getEntry('blog', slug)
if (!post) return Astro.redirect('/404')
const { Content } = await post.render()
Quelles erreurs courantes avec les Content Collections ? Comment les résoudre ?
Erreurs fréquentes :

1) MarkdownContentSchemaValidationError (frontmatter non conforme au Schema) :
• Champs manquants (compléter ou .optional())
• Fautes de frappe (uniformiser avec l'autocomplétion)
• Types incorrects (vérifier le format)

2) InvalidContentEntryFrontmatterError (erreur de syntaxe YAML) :
• Guillemets, deux-points, indentation
• Utilisez un éditeur avec validation YAML

3) Problèmes de format de date :
• z.coerce.date() plutôt que z.date() (YAML = chaînes uniquement)

4) Données héritées :
• .passthrough() pour assouplir temporairement — solution provisoire
• À long terme, uniformiser la structure du frontmatter

Checklist :
• Répertoire content/ existe
• content.config.ts au bon emplacement (pas dans content/)
• Clés collections = noms de répertoires
• Syntaxe YAML correcte
• Champs obligatoires remplis
• Types conformes au Schema
Comment organiser plusieurs collections ? Bonnes pratiques de conception Schema ?
Multi-collections :
• Blog, docs, études de cas : plusieurs collections (src/content/blog/, docs/, case-studies/)
• Dans content.config.ts :
const blogCollection = defineCollection({...})
const docsCollection = defineCollection({...})
• Export :
'blog': blogCollection,
'docs': docsCollection,
'case-studies': caseStudiesCollection
• Chaque collection peut avoir un Schema différent

Bonnes pratiques Schema :
1) Peu de champs obligatoires (seulement l'essentiel ; le reste .optional() ou .default())
2) Dates avec z.coerce.date()
3) Noms en camelCase (pubDate plutôt que pub_date)
4) Objets complexes : plusieurs collections liées par z.reference()
5) Commentaires clairs dans le Schema pour l'équipe

11 min de lecture · Publié le: 24 nov. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog