FAQ avec Astro Content Collections¶
Migration de la FAQ hardcodée vers Astro Content Collections + Markdown
Contexte¶
Actuellement, la FAQ est définie en JavaScript dans src/pages/fr/faq.astro (~150 lignes de données). Cette approche pose plusieurs problèmes :
- Maintenance difficile : Modifier une réponse nécessite d'éditer du code
- Duplication : Les versions FR et EN doivent être synchronisées manuellement
- Pas de CMS : Impossible d'utiliser Decap CMS pour éditer la FAQ
- Pas de Markdown : Impossible de formater les réponses (liens, listes, etc.)
Solution : Astro Content Collections¶
Astro Content Collections permet de :
- Stocker les FAQ en fichiers Markdown
- Valider le schema avec Zod (type-safety)
- Supporter le Markdown dans les réponses
- Faciliter l'édition via Decap CMS
- Gérer le multilingue proprement
Architecture cible¶
src/
content/
config.ts # Schema Zod
faq/
fr/
securite/
01-acces-documents.md
02-zero-knowledge.md
03-hebergement.md
04-immutabilite.md
05-mot-de-passe-perdu.md
legal/
01-valeur-legale.md
02-tribunal.md
03-duree-conservation.md
business/
01-bulletins-salaire.md
02-logiciels-paie.md
03-double-detention.md
04-professions-reglementees.md
usage/
01-gratuit.md
02-limites-gratuit.md
03-carte-bancaire.md
04-hors-ligne.md
apps/
01-plateformes.md
02-application-mobile.md
transfer/
01-transfert-probatoire.md
02-partage-sans-compte.md
data/
01-portabilite.md
02-format-export.md
03-suppression-compte.md
misc/
01-rgpd.md
02-open-source.md
03-disponibilite.md
en/
security/
01-document-access.md
02-zero-knowledge.md
...
pages/
fr/
faq.astro # Page utilisant la collection
en/
faq.astro
Schema Content Collection¶
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const faqCollection = defineCollection({
type: 'content',
schema: z.object({
// Question (titre affiché)
question: z.string(),
// Catégorie pour le regroupement
category: z.enum([
'security', // Sécurité & Confidentialité
'legal', // Valeur probatoire & conformité
'business', // Entreprises & Professionnels
'usage', // Fonctionnement & Utilisation
'apps', // Applications & Plateformes
'transfer', // Transfert & partage
'data', // Données & Portabilité
'misc' // Divers
]),
// Ordre d'affichage dans la catégorie
order: z.number().int().positive(),
// Langue (fr, en)
lang: z.enum(['fr', 'en']),
// Optionnel : clé de traduction pour lier FR/EN
translationKey: z.string().optional()
})
});
export const collections = {
faq: faqCollection
};
Format fichier FAQ Markdown¶
---
question: "ProbatioVault peut-il accéder à mes documents ?"
category: "security"
order: 1
lang: "fr"
translationKey: "security-document-access"
---
**Non.** ProbatioVault utilise une architecture **zero-knowledge** :
- Vos documents sont chiffrés sur votre appareil
- Le chiffrement se fait **avant** l'envoi
- Nous n'avons **jamais** accès à vos contenus
> La clé de déchiffrement n'existe que sur vos appareils.
Page FAQ refactorisée¶
---
// src/pages/fr/faq.astro
import { getCollection } from 'astro:content';
import BaseLayout from "../../layouts/BaseLayout.astro";
import Hero from "../../components/Hero.astro";
// Récupérer toutes les FAQ françaises
const allFaq = await getCollection('faq', ({ data }) => data.lang === 'fr');
// Grouper par catégorie
const categories = {
security: { title: "Sécurité & Confidentialité", icon: "🔐", items: [] },
legal: { title: "Valeur probatoire & conformité", icon: "⚖️", items: [] },
business: { title: "Entreprises & Professionnels", icon: "🏢", items: [] },
usage: { title: "Fonctionnement & Utilisation", icon: "👤", items: [] },
apps: { title: "Applications & Plateformes", icon: "📱", items: [] },
transfer: { title: "Transfert & partage", icon: "🔄", items: [] },
data: { title: "Données & Portabilité", icon: "💾", items: [] },
misc: { title: "Divers", icon: "🧠", items: [] }
};
// Trier et grouper
for (const entry of allFaq) {
const cat = entry.data.category;
if (categories[cat]) {
categories[cat].items.push(entry);
}
}
// Trier chaque catégorie par order
for (const cat of Object.values(categories)) {
cat.items.sort((a, b) => a.data.order - b.data.order);
}
// Générer le schema FAQPage pour SEO
const allQuestions = allFaq.map(entry => ({
"@type": "Question",
"name": entry.data.question,
"acceptedAnswer": {
"@type": "Answer",
"text": entry.body // Markdown brut ou compilé
}
}));
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": allQuestions
};
---
<BaseLayout
title="FAQ — Questions fréquentes | ProbatioVault"
description="Toutes les réponses à vos questions sur ProbatioVault"
ogImage="/og/faq-fr.png"
lang="fr"
jsonLd={faqSchema}
>
<Hero
title="Questions fréquentes"
subtitle="Tout ce que vous devez savoir sur ProbatioVault"
/>
<section class="section faq-content">
<div class="container">
{Object.entries(categories)
.filter(([_, cat]) => cat.items.length > 0)
.map(([key, category]) => (
<div class="faq-category" id={key}>
<h2 class="category-title">
<span class="category-icon">{category.icon}</span>
{category.title}
</h2>
<div class="faq-list">
{category.items.map(async (entry) => {
const { Content } = await entry.render();
return (
<details class="faq-item">
<summary class="faq-question">
<span class="question-text">{entry.data.question}</span>
<span class="toggle-icon" aria-hidden="true"></span>
</summary>
<div class="faq-answer">
<Content />
</div>
</details>
);
})}
</div>
</div>
))}
</div>
</section>
<!-- CTA section... -->
</BaseLayout>
Configuration Decap CMS¶
# public/admin/config.yml
collections:
- name: "faq-fr"
label: "FAQ (Français)"
folder: "src/content/faq/fr"
create: true
slug: "{{category}}/{{order}}-{{slug}}"
fields:
- { label: "Question", name: "question", widget: "string" }
- label: "Catégorie"
name: "category"
widget: "select"
options:
- { label: "Sécurité & Confidentialité", value: "security" }
- { label: "Valeur probatoire & conformité", value: "legal" }
- { label: "Entreprises & Professionnels", value: "business" }
- { label: "Fonctionnement & Utilisation", value: "usage" }
- { label: "Applications & Plateformes", value: "apps" }
- { label: "Transfert & partage", value: "transfer" }
- { label: "Données & Portabilité", value: "data" }
- { label: "Divers", value: "misc" }
- { label: "Ordre", name: "order", widget: "number", value_type: "int", min: 1 }
- { label: "Langue", name: "lang", widget: "hidden", default: "fr" }
- { label: "Clé traduction", name: "translationKey", widget: "string", required: false }
- { label: "Réponse", name: "body", widget: "markdown" }
- name: "faq-en"
label: "FAQ (English)"
folder: "src/content/faq/en"
create: true
slug: "{{category}}/{{order}}-{{slug}}"
fields:
# Mêmes champs avec lang: "en"
Avantages de cette approche¶
| Aspect | Avant (hardcodé) | Après (Content Collections) |
|---|---|---|
| Édition | Modifier du code JS | Éditer un fichier Markdown |
| Format réponses | Texte brut | Markdown complet |
| CMS | Non supporté | Decap CMS intégré |
| Type-safety | Non | Zod validation |
| Multilingue | Duplication manuelle | Structure claire FR/EN |
| Git history | Diff complexe | Diff par question |
| Collaboration | Dev uniquement | Non-devs via CMS |
Migration¶
Étapes¶
- Créer la structure
src/content/faq/fr/etsrc/content/faq/en/ - Définir le schema dans
src/content/config.ts - Migrer les FAQ existantes vers fichiers Markdown
- Refactoriser les pages
faq.astroFR et EN - Mettre à jour Decap CMS
config.yml - Supprimer le code hardcodé
Script de migration (optionnel)¶
Fichiers impactés¶
| Fichier | Action |
|---|---|
src/content/config.ts | Créer (schema) |
src/content/faq/fr/*.md | Créer (27 fichiers) |
src/content/faq/en/*.md | Créer (27 fichiers) |
src/pages/fr/faq.astro | Refactoriser |
src/pages/en/faq.astro | Refactoriser |
public/admin/config.yml | Ajouter collections FAQ |
Estimation¶
- Configuration schema : 15 min
- Migration FAQ FR : 30 min (27 fichiers)
- Migration FAQ EN : 30 min (27 fichiers)
- Refactorisation pages : 20 min
- Configuration CMS : 10 min
- Tests : 15 min
Total : ~2h