# Analyse du template `cms_portfolio` — cahier des charges fonctionnel

Analyse du rendu public de `ancien-code/sandbox/cms_portfolio/` (`index.php`,
`prestation.php`, `inc/*.php`) et inventaire fonctionnel de son admin
(`_data/admin.php`, `_data/save.php`). Sert de matière première à la
section 4 de `cdc-fonctionnel.md`.

**Hors périmètre de cette analyse** (non lus en détail, mentionnés seulement
quand ils recoupent un champ) : `mentions-legales.php`, `rdv-valide.php`,
`forms/*.php`, `HhfYihN/save.php`, `assets/js/main.js`, les fichiers
`*_back_*.php` (versions de sauvegarde, non le code actif). En particulier,
`ent_siret`, `ent_tva`, `ent_adeli`, `ent_rpps` et `ent_cp_ville` sont bien
affichés quelque part — sur `mentions-legales.php` — mais pas sur les pages
analysées ici ; ce n'est donc pas un écart "champ jamais affiché", juste un
affichage hors du périmètre demandé.

Le composant `inc/portfolio.php` a un fonctionnement suffisamment différent
du schéma cible pour mériter sa propre lecture attentive — voir la section
dédiée plus bas et le tableau d'écarts.

---

## 1. Sections visuelles, dans l'ordre d'apparition

### Page d'accueil (`index.php`)

#### 1.1 Header / nav (`inc/header.php`)
- Champs : `prenom`, `nom` (logo texte) · `telephone` (icône mobile) ·
  `prestations[].nom`, `prestations[].slug` (sous-menu déroulant "Tarifs &
  Prestations", une entrée par prestation définie)
- Un bloc de liens réseaux sociaux existe dans le HTML mais est désactivé
  (`d-none`, `href="#"` factices, pas de classe qui l'affiche) — markup mort,
  ne correspond à aucun champ JSON actuellement.
- Bibliothèques : Bootstrap (grille, icônes `bi-telephone`/`bi-geo-alt`/
  `bi-envelope`/`bi-chevron-down`/`bi-list`). Le sous-menu ne porte aucun
  attribut `data-bs-*` — son ouverture/fermeture est probablement pilotée par
  `assets/js/main.js` (hors périmètre), pas par le JS de Bootstrap
  lui-même : l'usage réel de `bootstrap.bundle.min.js` ici n'est pas confirmé
  par les fichiers analysés.

#### 1.2 Hero (`<header>` de l'article, `id="hero"`)
- Image principale : `assets/img/{slug_image}-1.webp` → correspond en
  position à `photos_accueil[0].image` (position 1 = hero, selon
  `client-json-explication.md`). **Écart de mécanisme** : l'attribut `alt`
  n'est pas lu depuis un champ stocké — il est recalculé à chaque affichage
  à partir de `prenom` + `nom` + dénomination + ville, alors que le schéma
  cible prévoit un `photos_accueil[].alt` librement saisi.
- Titre (h1) : `prenom`, `nom` (spans `givenName`/`familyName`) · dénomination
  professionnelle (voir écart ci-dessous) rendue **en HTML brut** (balise
  `<b>`).
- Spécialités : boucle sur des clés numérotées → `metier.specialites[]`.
- Bouton CTA (`inc/cta-button.php`) : lien + libellé → `cta.lien`, `cta.label`.
  **Écart de mécanisme** : l'icône n'est jamais stockée, elle est déduite à
  l'affichage du préfixe du lien (`tel:` / `mailto:` / sinon calendrier),
  alors que le schéma cible stocke `cta.icone` comme champ à part entière
  (avec valeur par défaut `bi-calendar-date`).
- Boutons d'accès rapide (`inc/quick-access-buttons.php`, réutilisé aussi en
  section Approche) : liens statiques codés en dur vers `#faq` et
  `#alt-services`, aucun champ JSON — simple aide à la navigation.
- 3 blocs "Pourquoi moi" (C-A-B) : titre+texte → `metier.description`,
  `metier.caracteristiques`, `metier.avantages`, `metier.benefices` (chacun
  `{titre, texte}`).
- Un bloc "Recommandation" (partage Messenger/WhatsApp/SMS) existe en
  commentaire HTML dans le code — inactif, ne s'affiche jamais.
- Bibliothèques : AOS (`fade-in`, `fade-down`, `zoom-out` sur presque tous
  les blocs), Bootstrap (grille, icônes, pictos métier statiques en PNG hors
  JSON).

#### 1.3 Biographie (`id="biographie"`)
- Photo : `assets/img/{slug_image}-2.webp` → `photos_accueil[1].image`
  (position 2 = illustration biographie, conforme à l'ordre documenté). Même
  écart d'`alt` recalculé que le hero (jamais lu depuis
  `photos_accueil[].alt`).
- Titre de section "Formation et parcours" : texte statique du template, pas
  un champ client.
- Intro : `biographie.intro` (HTML brut).
- Items en accordéon (nombre libre) : `biographie.items[].titre`,
  `biographie.items[].texte`.
- Bibliothèques : AOS uniquement.

#### 1.4 Citation (bloc global, `inc/citation.php`)
- Texte + auteur d'une citation globale, réutilisée telle quelle en repli
  dans les pages prestation quand celles-ci n'ont pas leur propre citation
  (voir 1.10). **Correspondance déduite, pas nommée à l'identique** : la
  structure et la description de `metier.citation` dans
  `client-json-explication.md` ("même structure que la citation d'une
  prestation, pour ne pas perdre l'attribution") correspondent exactement à
  ce rôle de citation globale/de repli → `metier.citation.texte`,
  `metier.citation.auteur`. À confirmer avec Christophe avant de l'acter,
  puisque la clé n'est jamais nommée `metier_citation` dans l'ancien
  système.
- Aucune bibliothèque JS, image statique (pictogramme citation, non lié au
  JSON).

#### 1.5 Approche métier (`id="approche"`)
- Titre de section "Mon approche" : statique, pas un champ.
- Intro, Vision, Mission, Valeurs : `approche.intro`, `approche.vision`,
  `approche.mission`, `approche.valeurs` (chacun `{titre, texte}` sauf intro).
- Photo : `assets/img/{slug_image}-3.webp` → `photos_accueil[2].image`
  (position 3 = illustration approche, conforme à l'ordre documenté). Même
  écart d'`alt` recalculé.
- Boutons d'accès rapide (réutilisation du composant 1.2).
- Bibliothèques : AOS.

#### 1.6 Prestations — grille de cartes (`id="alt-services"`)
- Boucle sur 6 emplacements fixes (`prest_01` à `prest_06`, jamais plus) →
  `prestations[]`. **Écart** : le schéma cible n'impose aucune limite de
  longueur au tableau `prestations` ; le template plafonne à 6 par
  construction (boucle bornée, pas de détection dynamique de fin de liste
  comme ailleurs).
- Champs par carte : `prestations[].nom`, `prestations[].slug` (URL),
  `prestations[].descriptif_court`.
- Microdonnées schema.org `Service` par carte : réutilise `ent_ville` /
  `adresse.ville` pour `areaServed`, rien de nouveau côté champs.
- Bibliothèques : AOS (`zoom-in`), Bootstrap (grille).

#### 1.7 Témoignages (`inc/testimonials.php`, inclus aussi sur `prestation.php`)
- Champs : `avis[].prenom`, `avis[].nom`, `avis[].texte`.
- **Écart** : les 5 étoiles affichées sont codées en dur pour chaque avis
  (toujours 5 pleines), jamais lues depuis une donnée — alors que le schéma
  cible prévoit `avis[].note` (1 à 5) comme champ optionnel. Ce champ n'a
  aucun support dans le template ni dans l'admin actuels.
- **Écart** : `avis[].qualification` (schéma cible) n'existe pas non plus
  dans le template ni dans l'admin — aucune donnée de ce type n'est
  aujourd'hui saisie ni affichée.
- Bibliothèques : Swiper (carrousel, config JSON embarquée). Pas d'AOS sur
  cette section (le carrousel gère sa propre animation).

#### 1.8 FAQ page d'accueil (`id="faq"`)
- Paragraphe d'intro statique du template (pas un champ).
- Champs : `faq[].question`, `faq[].reponse` (boucle non bornée, contrairement
  au JSON-LD FAQPage du `<head>` qui, lui, est plafonné à 10 — voir 1.11).
- Bibliothèques : AOS.

#### 1.9 Contact (`id="contact"`)
- Bloc "Rendez-vous" : utilise `ent_lien_rdv`, une clé **distincte** de
  `cta_lien` utilisé dans le hero. Le schéma cible n'a qu'un seul objet
  `cta` — `client-json-explication.md` documente déjà explicitement cette
  consolidation ("Remplace toutes les anciennes conventions concurrentes...
  `ent_lien_rdv`..."), donc ce n'est pas une découverte nouvelle mais une
  confirmation que la fusion prévue couvre bien ce cas rencontré dans le
  template.
- Téléphone, email : `telephone`, `email` → l'email est obfusqué en base64
  et décodé en JS au clic/affichage (anti-scraping) ; comportement à
  reproduire si on veut garder cette protection, l'ancien code ne se
  contente pas d'un simple `mailto:` en clair.
- Adresse structurée (microdonnées cachées) : `adresse.rue`,
  `adresse.ville`, `adresse.code_postal`, `adresse.pays`.
- Carte 1 : `ent_googlemap_iframe` (code iframe complet, rendu en HTML brut)
  → `google_maps_embed` (nouveau schéma : uniquement l'URL `src`, le
  template porte la balise `<iframe>`). Changement déjà acté dans
  `client-json-explication.md`, cohérent, pas un écart à trancher.
- **Écart réel** : `ent_googlemap_link` (une URL de partage Google Maps
  distincte de l'iframe, utilisée uniquement pour la propriété schema.org
  `hasMap`) n'a **aucun équivalent** dans `client-json-explication.md`, qui
  ne connaît que `google_maps_embed`. Sans ce champ, la propriété `hasMap`
  du balisage `LocalBusiness` n'a plus de source de données.
- Carte 2 (adresse secondaire, `ent_ville_2` + `ent_googlemap_iframe_2`) :
  **écart réel** — le template gère une implantation secondaire complète
  (adresse, téléphone, horaires, carte), alors que
  `client-json-explication.md` est explicite : *"Une seule représentation,
  jamais de `adresse_2` secondaire."* Capacité existante dans l'ancien
  système, sciemment exclue du schéma cible par décision déjà actée — à
  signaler à Christophe comme perte de fonctionnalité assumée, pas comme
  oubli.
- Bibliothèques : AOS.

#### 1.10 Footer (`inc/footer.php`)
- `prenom`, `nom`, `telephone`, `email` (même obfuscation base64 qu'en 1.9).
- Liste prestations : `prestations[].slug`, `prestations[].nom`.
- Partenaires (bloc conditionnel, affiché seulement si au moins un
  partenaire existe) : `liens_partenaires[].label` (`_texte` dans l'ancien
  système, renommage direct), `liens_partenaires[].url`.
- Lien "Mentions légales" → page statique séparée (`mentions-legales.php`,
  hors périmètre de cette analyse), pas un champ JSON.
- Crédits : `credits.interviewer.{nom,lien}`, `credits.photographe.{nom,lien}`,
  `credits.graphiste.{nom,lien}` (renommage direct depuis les clés plates
  `interviewer`/`interviewer_lien` etc.).
- Texte de copyright / mention légale de protection : statique, pas un champ.
- Aucune bibliothèque JS.

#### 1.11 Open Graph (`inc/open-graph.php`, dans le `<head>`)
- `og:title`/`og:description`/`og:url` ← `meta_accueil.{titre,description}`
  ou `prestations[].meta.{titre,description}` selon la page appelante.
- `og:image` ← même convention que la photo hero (`photos_accueil[0].image`,
  implicite via `slug_image`, pas stocké par page).
- `og:site_name` ← `prenom` + `nom` + dénomination (voir écart dénomination
  ci-dessous).

#### 1.12 Balisage schema.org JSON-LD (`<head>` de `index.php` uniquement)
Trois blocs : `LocalBusiness`, `Person`, `FAQPage`.
- Réutilise la plupart des champs déjà cités (`prenom`, `nom`, dénomination,
  `metier.description.texte`, `telephone`, `email`, adresse, `biographie.intro`).
- `url` (LocalBusiness et Person) ← `ent_url_base`, une clé qui n'existe plus
  dans le schéma cible (pas de champ "domaine principal" séparé de
  `domaines[]`) — cohérent avec la décision déjà documentée de calculer le
  canonical depuis la route plutôt que de le stocker ; pas un écart à
  trancher, juste une confirmation.
- `hasMap` ← `ent_googlemap_link` — écart déjà signalé en 1.9.
- `sameAs` (les deux blocs) ← `ent_autre_url_01/02/03`. **Écart réel** :
  aucun champ de `client-json-explication.md` ne couvre des URLs de profils
  externes/réseaux sociaux. Si ce balisage `sameAs` doit être conservé, il
  manque un champ (tableau d'URLs) dans le schéma cible.
- `openingHoursSpecification` : **codé en dur** (Lundi-Vendredi 9h-19h),
  totalement déconnecté du champ `horaires` (texte libre) qui existe par
  ailleurs. Le balisage structuré ne reflète donc jamais les horaires réels
  du client — écart fonctionnel (pas un manque de champ JSON, un défaut du
  template lui-même).
- `acceptsReservations: true` : codé en dur, jamais lu depuis une donnée.
- `hasOfferCatalog` / `itemListElement` : reconstruit depuis les 6
  prestations maximum (même plafond qu'en 1.6), utilise
  `prestations[].{nom,descriptif_court,tarif}` + un champ `seo_canonical` qui
  disparaît du schéma cible (cf. remarque canonical ci-dessus).
- `FAQPage.mainEntity` : boucle plafonnée à **10** questions (`faq_01` à
  `faq_10`), alors que la section HTML visible (1.8) n'a elle-même aucune
  limite — incohérence interne à l'ancien système à connaître : au-delà de
  10 FAQ, le balisage structuré et l'affichage visible divergeraient déjà
  aujourd'hui.

### Page prestation (`prestation.php`)

Reprend header (1.1), portfolio (section dédiée ci-dessous), témoignages
(1.7) et footer (1.10). Sections propres :

#### 1.13 Titre / fil d'Ariane
- `prestations[].nom` (titre h1 + dernier élément du fil d'Ariane). Pas
  d'écart.

#### 1.14 Informations pratiques (4 cartes conditionnelles)
- `prestations[].public_concerne`, `.lieu_intervention`, `.duree`, `.tarif`
  — chacune n'apparaît que si renseignée. Correspondance directe et propre.

#### 1.15 Indications (bloc conditionnel)
- `prestations[].indications.{titre,texte}` + bouton CTA (réutilisation du
  composant 1.2). Correspondance directe.

#### 1.16 Bloc CAB (citation + contenu)
- Citation : `prestations[].citation.{texte,auteur}`, avec repli vers la
  citation globale (1.4) si absente. **À trancher** : ce mécanisme de repli
  n'est documenté nulle part dans `client-json-explication.md` — le champ
  `prestations[].citation` y est simplement optionnel, sans préciser si son
  absence doit déclencher un repli automatique vers `metier.citation` ou
  simplement ne rien afficher.
- Titre + intro de la section CAB (`cab_titre`/`cab_intro` dans l'ancien
  système) : **correspondance déduite, pas certaine** — le nom le plus
  proche dans le schéma cible est `prestations[].accroche.{titre,texte}`
  (un "accroche" avant les items C-A-B), mais aucun des deux documents ne
  fait le lien explicitement. À confirmer.
- Item "C" (`car_titre`/`car_texte` dans l'ancien système, pour
  "Caractéristiques") : **correspondance déduite, pas certaine** — le champ
  le plus proche du schéma cible est `prestations[].deroulement`, un nom
  différent qui ne réfère plus explicitement à "caractéristiques". À
  confirmer que le sens attendu est resté le même.
- Item "A" (avantages) : `prestations[].avantages.{titre,texte}` —
  correspondance directe et sûre.
- Item "B" (bénéfices) : `prestations[].benefices.{titre,texte}` —
  correspondance directe et sûre.
- Bouton CTA (réutilisation 1.2).
- Bibliothèques : AOS.

#### 1.17 Déclencheur (bloc conditionnel)
- `prestations[].declencheur.{titre,texte}` + bouton CTA. Correspondance
  directe.

#### 1.18 Portfolio par prestation (`inc/portfolio.php`) — écarts structurels majeurs

C'est la section où le template s'éloigne le plus du schéma cible :

- **Titre et intro de section** (`{prest}_portfolio_titre`,
  `{prest}_portfolio_intro`) commandent l'affichage même de la section : elle
  n'apparaît que si un titre est renseigné. **Le schéma cible n'a aucun champ
  pour un titre ou une intro de section portfolio** — `prestations[].portfolio`
  est un simple tableau d'items, et sa présence (tableau non vide) est le
  seul déclencheur d'affichage prévu. Écart réel : si on veut garder un
  titre/intro personnalisable par le client, il manque un champ.
- **Filtres** : l'ancien système stocke, pour chaque filtre, un **libellé
  affiché** distinct de son **slug CSS** (ex. libellé "Portails" / slug
  `portails`), saisis séparément par le client, jusqu'à 6 filtres. Le schéma
  cible ne stocke aucune liste de filtres : les boutons de filtre "se
  déduisent dynamiquement des valeurs de `categorie` présentes" dans les
  items. **Écart réel** : la valeur brute de `categorie` sert alors à la
  fois de filtre technique et de libellé affiché — la possibilité d'un
  libellé différent du slug (accents, majuscules, formulation plus soignée)
  disparaît, sauf à considérer que `categorie` peut désormais contenir
  directement un libellé lisible.
- **Items** : `{prest}_folio_NN_titre/desc/img/filtre` → correspond à
  `prestations[].portfolio[].{titre,description,image,categorie}` (renommage
  direct et sûr pour ces 4 clés).
- **Champs du schéma cible sans aucun équivalent dans l'ancien système** :
  `portfolio[].alt` et `portfolio[].tarif` n'existent ni dans le template ni
  dans l'admin — ce sont des capacités neuves, sans donnée à migrer (l'`alt`
  affiché aujourd'hui reprend simplement le titre de l'item, jamais un champ
  dédié).
- **Convention de chemin d'image différente** : l'ancien système range les
  images par `assets/img/portfolio/{prestation}/{slug-filtre}/{fichier}`
  (sous-dossier par filtre en plus du sous-dossier par prestation), alors que
  `client-json-explication.md` / l'arborescence cible ne prévoit qu'un
  sous-dossier par prestation (`.../portfolio/{slug-prestation}/`), sans
  niveau filtre. Changement de convention à noter, cohérent avec la
  décision déjà actée que le portfolio n'est jamais déduit du système de
  fichiers.
- Bibliothèques : Isotope + imagesloaded (tri/filtrage), GLightbox
  (agrandissement des images). **AOS est volontairement désactivé** à
  l'intérieur de cette grille — un commentaire du code explique que
  l'animation provoquerait un flash blanc au filtrage — alors qu'AOS reste
  chargé et actif ailleurs sur la même page.
- **Dépendance chargée mais pas toujours utilisée** : GLightbox, Isotope et
  imagesloaded sont chargés en `<script>` sur *toutes* les pages
  (`index.php` et `prestation.php`), y compris celles où aucune section
  portfolio ne s'affiche (le composant se masque lui-même si
  `{prest}_folio_01_img` est vide) — poids mort sur ces pages-là.

---

## 2. Récapitulatif des écarts (les deux sens)

### A. Le template affiche un contenu sans champ correspondant dans `client-json-explication.md`
- `ent_googlemap_link` (URL de partage Google Maps, utilisée pour
  `hasMap` en schema.org) — aucun champ équivalent, seul `google_maps_embed`
  (l'iframe) existe côté schéma.
- `ent_autre_url_01/02/03` (URLs "sameAs" en schema.org, réseaux
  sociaux/profils externes) — aucun champ équivalent.
- Adresse secondaire complète (rue, CP, ville, pays, téléphone, horaires,
  carte Google Maps dédiée) — exclue par décision déjà actée
  ("jamais de `adresse_2` secondaire"), à confirmer comme perte assumée.
- Libellé de filtre portfolio distinct du slug de catégorie — le schéma
  cible déduit tout de `categorie`, sans libellé personnalisable séparé.
- Titre et intro de section portfolio par prestation — aucun champ dans le
  schéma cible pour piloter ce texte.
- Bloc de partage social ("Recommandation") dans le hero — inactif
  (commenté) dans le template lui-même, donc sans urgence, mais à noter.

### B. Un champ du schéma JSON n'a pas (ou plus) de section pour l'afficher dans ce template
- `avis[].note` (1 à 5) — aucun affichage dynamique, les étoiles sont
  codées en dur à 5/5 dans le template actuel.
- `avis[].qualification` — aucun champ ni affichage dans l'ancien système.
- `portfolio[].alt` — aucun champ dédié ; l'ancien système réutilise le
  titre comme texte alternatif.
- `portfolio[].tarif` — aucun champ ni affichage dans l'ancien système.
- `prestations[].pages_liees[]` — **absent intégralement** de l'ancien
  système : ni le template, ni l'admin ne connaissent la moindre notion de
  sous-page de prestation invisible du menu avec lien retour. Aucun
  comportement existant à reprendre pour cette capacité — à construire sans
  référence.
- Champs d'infrastructure sans affichage visuel direct, absents de l'ancien
  système par nature (normal, ce sont des champs propres à la nouvelle
  architecture multi-tenant) : `google_site_verification`, `llm_txt`,
  `llm_full_txt`, `ville_seo`, `publie`, `domaines[]`, `template`, `slug`.

### C. Correspondances à confirmer explicitement (pas assez sûres pour être actées telles quelles)
- `ent_denomination` → candidat le plus probable : `metier.libelle`. Mais
  `ent_denomination` est utilisé de façon bien plus étendue dans l'ancien
  système (h1, `jobTitle` schema.org, `og:site_name`, description
  LocalBusiness) qu'un simple champ d'affichage nommé une fois — à valider
  avant de l'acter comme équivalence.
- `citation` / `auteur_citation` (globaux) → candidat le plus probable :
  `metier.citation.{texte,auteur}`, cohérent avec la description du champ
  dans `client-json-explication.md`, mais jamais nommé ainsi dans l'ancien
  système.
- `{prest}_cab_titre` / `{prest}_cab_intro` → candidat le plus probable :
  `prestations[].accroche`.
- `{prest}_car_titre` / `{prest}_car_texte` (le "C" de Caractéristiques) →
  candidat le plus probable : `prestations[].deroulement` — changement de
  nom qui pourrait aussi signaler un changement de sens, à vérifier.
- Mécanisme de repli "citation de prestation vide → citation globale" — le
  comportement existe dans l'ancien système mais n'est spécifié dans aucun
  des deux documents de référence pour le nouveau.

---

## 3. Inventaire fonctionnel de `_data/admin.php` / `_data/save.php`

Uniquement les capacités offertes aujourd'hui, en langage fonctionnel — pas
de référence à leur code ni à leur structure interne.

### Champs modifiables par formulaire
- **Identité personnelle** : prénom, nom, dénomination professionnelle, nom
  commercial.
- **Spécialités** : liste de champs texte affichés un par un, un nouveau
  champ apparaît quand le précédent est rempli.
- **Numéros professionnels** : SIRET, TVA, ADELI, RPPS.
- **Citation mise en avant (globale)** : texte + auteur.
- **Adresse principale** : rue, code postal, ville, "CP + Ville" (affiché,
  saisi séparément des deux champs bruts), pays.
- **Contact** : téléphone, email, horaires (texte enrichi).
- **URLs** : domaine principal, lien de prise de rendez-vous, jusqu'à 3
  "autres URLs".
- **Adresse secondaire (optionnelle)** : mêmes champs qu'une adresse
  principale (rue, CP, ville, "CP+Ville", pays, téléphone, horaires) plus
  sa propre carte Google Maps (lien + iframe).
- **Carte Google Maps principale** : lien de partage + code iframe complet.
- **Bouton CTA** : lien + libellé (l'icône est déterminée automatiquement
  selon le préfixe du lien, jamais saisie).
- **Métier** : 4 blocs titre + texte (description, caractéristiques,
  avantages, bénéfices).
- **Biographie** : intro + éléments ajoutés dynamiquement (titre + texte).
- **Approche** : intro + vision/mission/valeurs (chacun titre + texte).
- **Jusqu'à 6 prestations**, chacune avec : nom, slug d'URL, description
  courte, durée, tarif (HTML autorisé), public concerné, lieu
  d'intervention, un bloc "Indications" (titre + texte), une introduction de
  section CAB (titre + texte), trois blocs C/A/B (chacun titre + texte), un
  bloc "Déclencheur" (titre + texte), une citation propre optionnelle
  (texte + auteur) avec aide indiquant qu'à défaut la citation globale sera
  utilisée à l'affichage.
- **Portfolio par prestation** : un titre de section + une intro
  (l'aide précise explicitement que la section n'apparaît sur le site que
  si le titre est rempli), jusqu'à 6 filtres (chacun avec un libellé affiché
  et un slug CSS saisis séparément), jusqu'à 12 réalisations (titre,
  description courte, nom de fichier image, slug de filtre associé).
- **Avis clients** : liste dynamique de prénom, nom, texte de l'avis.
- **FAQ (page d'accueil)** : liste dynamique de question + réponse (HTML
  autorisé).
- **SEO** : un identifiant global servant à nommer les fichiers image, puis
  pour la page d'accueil et chaque prestation : title, meta description,
  mots-clés, URL canonique — avec un compteur de caractères visuel
  (title/meta description) qui prévient au-delà d'un seuil sans jamais
  bloquer la saisie.
- **Partenaires** : liste dynamique de texte de lien + URL.
- **Crédits** : interviewer, photographe, graphiste (chacun nom + lien).

### Types d'upload gérés
**Aucun.** Tous les champs ci-dessus sont des champs texte. Les "images"
(photo hero, photo biographie, photo approche, images de portfolio) sont
toutes référencées par un nom ou chemin de fichier saisi à la main — le
dépôt du fichier lui-même se fait entièrement en dehors de cette interface.
Il n'existe aujourd'hui aucune capacité d'upload de fichier dans
l'administration existante.

### Validations appliquées
Aucune validation de contenu, ni côté formulaire (le seul contrôle visuel
est le compteur de caractères SEO, purement informatif et non bloquant) ni
côté enregistrement. L'enregistrement vérifie uniquement que la requête
contient du JSON syntaxiquement valide et que le fichier cible est
accessible en écriture — aucun contrôle de format (email, URL, téléphone),
de champ obligatoire, de doublon d'identifiant, ou de type de fichier
n'existe.

### Capacités de l'ancien système absentes de `cdc-fonctionnel.md` section 2
- **Compteur de caractères SEO en temps réel** (title / meta description),
  avec seuils visuels d'alerte progressive — absent de la section 2
  actuelle.
- **Repli automatique** de la citation d'une prestation vers la citation
  globale quand le champ propre à la prestation est vide — comportement
  existant, non spécifié pour le nouveau système (voir aussi section 2.C
  ci-dessus).
- **Libellé de filtre portfolio distinct de sa catégorie technique** — non
  couvert par la section 2 actuelle, qui ne détaille pas le mécanisme de
  filtre.

Tout le reste des capacités listées dans la section 2 de
`cdc-fonctionnel.md` (authentification, import JSON, refus de doublon de
slug, édition de tous les champs texte, upload des 3 photos d'accueil,
upload + métadonnées portfolio, action "Publier", aperçu brouillon) couvre
déjà ce que l'ancien système ne fait pas — rien à ajouter de ce côté.
