Aller au contenu

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 :

  1. Stocker les FAQ en fichiers Markdown
  2. Valider le schema avec Zod (type-safety)
  3. Supporter le Markdown dans les réponses
  4. Faciliter l'édition via Decap CMS
  5. 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

  1. Créer la structure src/content/faq/fr/ et src/content/faq/en/
  2. Définir le schema dans src/content/config.ts
  3. Migrer les FAQ existantes vers fichiers Markdown
  4. Refactoriser les pages faq.astro FR et EN
  5. Mettre à jour Decap CMS config.yml
  6. Supprimer le code hardcodé

Script de migration (optionnel)

// scripts/migrate-faq.js
// Génère les fichiers MD depuis le JS existant

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


← Retour INDEX