# Contrat de schéma — Covalba WordPress headless

Source de vérité du modèle de contenu. Toute évolution passe par une PR qui met à jour
**en même temps** : ce fichier, le mu-plugin `covalba-core` (PHP) et `src/lib/wp/` +
`src/components/blocks/` (Next). **On ne renomme jamais un `acf_fc_layout` publié.**

## Conventions

- Noms de champs ACF : `snake_case` français. `graphql_field_name` : `camelCase`.
- Field keys déterministes : `field_cvb_{layout}_{champ}` (sous-champs : `field_cvb_{layout}_{champ}_{sous_champ}`).
- Le flexible content unique s'appelle `sections` (key `field_cvb_sections`), field group
  « Sections de page » (`group_cvb_sections`), `graphql_field_name: sections`,
  localisé sur : `page`, `produit`, `toiture`, `industrie`, `reference`.
- WPGraphQL for ACF v2 expose chaque layout en type GraphQL dont le nom **se termine par**
  `{LayoutPascal}Layout` (ex. layout `grille_cards` → type `...GrilleCardsLayout`).
  Le BlockRenderer matche par suffixe, jamais par nom complet.
- Tout champ image : `return_format = array` côté ACF ; côté GraphQL on lit
  `image { node { sourceUrl altText mediaDetails { width height } } }`.
- Les repeaters/flexibles vides sortent `null` en GraphQL → toujours mapper `?? []`.
- Titres : la convention `*texte*` marque le span d'accent (rendu `<span class="accent">` côté front).

## CPT

| CPT | slug | rewrite | graphql single/plural | supports | menu_icon |
|---|---|---|---|---|---|
| Produit | `produit` | `solutions` | `produit` / `produits` | title, thumbnail, revisions | dashicons-admin-home |
| Toiture | `toiture` | `toitures` | `toiture` / `toitures` | title, thumbnail, revisions | dashicons-admin-multisite |
| Industrie | `industrie` | `industries` | `industrie` / `industries` | title, thumbnail, revisions | dashicons-building |
| Référence | `reference` | `references` | `reference` / `references` | title, thumbnail, revisions | dashicons-awards |
| Lieu | `lieu` | `lieux` | `lieu` / `lieux` | title, thumbnail, revisions | dashicons-location-alt |
| Formulaire HubSpot | `formulaire_hubspot` | `formulaires-hubspot` | `formulaireHubspot` / `formulairesHubspot` | title, thumbnail, revisions | dashicons-feedback |

Tous : `public => true`, `publicly_queryable => false` (pas de rendu WP), `show_in_rest => true`,
`show_in_graphql => true`, `has_archive => false`, `rewrite => ['with_front' => false]`.
Le CPT `lieu` ne porte pas le flexible `sections` : il alimente le modèle Next programmatique
des pages locales à la racine (`/cool-roof-lyon`, `/cool-roof-rhone`, etc.) via `ficheLieu`.
Les one-off (home, contact, qui-sommes-nous, devenir-applicateur, bat-en-112, guide-cool-roof,
simulateur-economie-energie, faq, diagnostic, estimation, hub industrie, listing references) = **pages natives WP**.

## Taxonomies

| Taxonomie | slug | graphql | attachée à | termes initiaux |
|---|---|---|---|---|
| Secteur | `secteur` | `secteur` / `secteurs` | reference, industrie | industrie, logistique, distribution, tertiaire, collectivites, agricole, erp |
| Type de support | `type_support` | `typeSupport` / `typesSupport` | reference, produit, toiture | bitume, bac-acier, fibrociment, tuiles-ciment, toiture-plate, epdm, pvc, zinc |

## Clones (sous-structures partagées)

### `clone_entete_section` (group `group_cvb_clone_entete`)
| champ | type | graphql |
|---|---|---|
| badge | text | badge |
| titre | text (`*accent*` autorisé) | titre |
| intro | textarea | intro |

### `clone_cta` (group `group_cvb_clone_cta`)
| champ | type | graphql |
|---|---|---|
| label | text | label |
| lien | text (url ou path interne) | lien |
| style | select: primary / secondary / ghost | style |

### `clone_seo` (group `group_cvb_seo`, field group séparé « 🔍 SEO », position high, sur tous les types + pages)
| champ | type | graphql |
|---|---|---|
| titre_seo | text | titreSeo |
| meta_description | textarea rows 3 | metaDescription |
| canonical | url | canonical |
| og_image | image | ogImage |
| noindex | true_false | noindex |

Group exposé en GraphQL sous `seo`.

## Layouts génériques (disponibles sur tous les types) — 21

Chaque layout a `entete` (clone `clone_entete_section`, graphql `entete`) sauf mention contraire.

1. **hero** — variante (select: home/standard/compact), eyebrow (text), titre (text), lead (textarea), image (image), stats (repeater max 4: value text, label text), badges (repeater: texte text), cta_primaire (clone_cta, graphql ctaPrimaire), cta_secondaire (clone_cta, graphql ctaSecondaire). Pas d'entete.
2. **barre_reassurance** — items (repeater max 6: texte text). Pas d'entete.
3. **grille_cards** — entete, cards (repeater: icone (select, liste blanche lucide), titre text, texte textarea, image image, lien text), colonnes (select: 2/3/4), theme (select: clair/sombre/accent), transition (textarea).
4. **texte_image** — entete, contenu (wysiwyg), image (image), position_image (select: gauche/droite, graphql positionImage), liste (repeater: texte text), note (textarea), cta (clone_cta).
5. **chiffres** — entete, figures (repeater max 6: value text, label text, sublabel text), theme (select: clair/sombre).
6. **tableau_comparatif** — entete, entetes_colonnes (repeater: texte text, graphql entetesColonnes), lignes_comparatif (repeater, graphql lignesComparatif: cellules (repeater: texte text) — conventions `oui`/`non`/`-`), colonne_mise_en_avant (number, graphql colonneMiseEnAvant), texte_apres (textarea, graphql texteApres), cta (clone_cta).
7. **tableau_situations** — entete, lignes_situations (repeater, graphql lignesSituations: situation text, recommandation text, justification textarea), cta (clone_cta).
8. **etapes** — entete, etapes (repeater: titre text, texte textarea, image image), reassurance (textarea), video_url (url, graphql videoUrl).
9. **faq** — entete, questions (repeater: question text, reponse wysiwyg), json_ld (true_false défaut true, graphql jsonLd), cta (clone_cta).
10. **cta** — variante (select: mid/final/banner), titre (text), texte (textarea), reassurances (repeater: texte text), cta_primaire (clone_cta, graphql ctaPrimaire), cta_secondaire (clone_cta, graphql ctaSecondaire), formulaire_hubspot (relationship → formulaire_hubspot, graphql formulaireHubspot), hubspot_form_id (text legacy, graphql hubspotFormId). Pas d'entete.
11. **temoignages_video** — entete, temoignages (repeater: video_url url (graphql videoUrl), citation textarea, nom text, role text, entreprise text, contexte textarea).
12. **video** — entete, video_url (url, graphql videoUrl), points_cles (repeater: label text, texte textarea — graphql pointsCles).
13. **logos** — entete, source (select: clients/presse/custom), logos_custom (repeater: image image, nom text, lien text — graphql logosCustom).
14. **citation** — texte (textarea), auteur (text), role (text), photo (image). Pas d'entete.
15. **contenu_seo** — intro (wysiwyg), sections (repeater: titre text, contenu wysiwyg), sources (repeater: label text, url url), replie (true_false défaut true). Pas d'entete (le H2 vit dans sections).
16. **compatibilite_supports** — entete, supports (repeater: type text, detail textarea, image image), note_couverture (textarea, graphql noteCouverture), rendu (select: tabs/grille).
17. **grille_secteurs** — entete, mode (select: auto/manuel), secteurs (relationship → industrie, graphql secteurs).
18. **grille_solutions** — entete, produits (relationship → produit), affichage (select: cards/compact).
19. **references_grille** — entete, mode (select: toutes/secteur/manuel), secteur (taxonomy term select secteur), limite (number), references_manuel (relationship → reference, graphql referencesManuel), filtres (true_false, graphql filtres).
20. **timeline** — entete, jalons (repeater: annee text, titre text, texte textarea, image image).
21. **composant_react** — composant (select: simulateur_energie / diagnostic_toiture / estimation / climate_dashboard / applicateur_screener / formulaire_hubspot / carte_intervention), titre (text), formulaire_hubspot (relationship → formulaire_hubspot, graphql formulaireHubspot), hubspot_form_id (text legacy, graphql hubspotFormId). Pas d'entete. **Escape hatch** : monte un composant interactif codé côté front.

## Layouts spécifiques — 5 (filtrés par type côté BO via acf/load_field, présents dans le même flexible)

22. **specs_techniques** (produit) — entete, colonnes_specs (repeater, graphql colonnesSpecs: nom text), lignes_specs (repeater, graphql lignesSpecs: label text, valeurs (repeater: texte text)), colonne_mise_en_avant (number, graphql colonneMiseEnAvant), fiche_technique (file, graphql ficheTechnique).
23. **certifications** (produit) — entete, items_certifications (repeater, graphql itemsCertifications: logo image, nom text, description textarea).
24. **variantes_produit** (produit) — entete, variantes (repeater: nom text, description textarea, garantie text, sri text, usage text).
25. **avant_apres** (reference, toiture, produit) — entete, image_avant (image, graphql imageAvant), image_apres (image, graphql imageApres), legende_avant (text, graphql legendeAvant), legende_apres (text, graphql legendeApres).
26. **fiche_chantier** (reference) — afficher (true_false défaut true). Rend les données du groupe `ficheReference` du post courant.

### Filtrage BO (n'affecte pas le schéma GraphQL)
- `page` : tout sauf specs_techniques, variantes_produit, fiche_chantier
- `produit` : tout sauf fiche_chantier
- `toiture` : tout sauf specs_techniques, certifications, variantes_produit, fiche_chantier
- `industrie` : tout sauf specs_techniques, certifications, variantes_produit, fiche_chantier, avant_apres
- `reference` : tout sauf specs_techniques, variantes_produit, grille_secteurs

## Groupes de champs structurés (hors flexible)

### `ficheProduit` (group `group_cvb_fiche_produit`, sur produit, graphql `ficheProduit`)
product_name text (productName), tagline text, accent_color text hex (accentColor), garantie text,
sri text, prix text, fiche_technique_pdf file (ficheTechniquePdf).

### `ficheReference` (group `group_cvb_fiche_reference`, sur reference, graphql `ficheReference`)
client_name text (clientName), location text, produit relationship→produit (produit),
support text, surface text, metrics repeater (value text, label text), quote group (texte textarea,
auteur text, role text), video_url url (videoUrl), show_in_grid true_false (showInGrid).

### `article` (group `group_cvb_article`, sur le post type natif `post`, graphql `article`)
Articles de blog (`/blog/<slug>`). Le corps est rédigé dans l'**éditeur classique**
(post_content HTML, Gutenberg désactivé sur `post` via `use_block_editor_for_post_type`) ;
deux champs structurés dédiés en plus du corps :
en_bref repeater (point textarea) (enBref) — synthèse en tête d'article ;
bibliographie repeater (reference textarea APA, url url) (bibliographie) — sources en pied.
Le groupe SEO (`group_cvb_seo`) est aussi étendu à `post` (titre_seo, meta_description, canonical, og_image, noindex).
Image à la une native (`featuredImage`) + `post-thumbnails` activé (headless, pas de thème).

### `ficheLieu` (group `group_cvb_fiche_lieu`, sur lieu, graphql `ficheLieu`)
Identité : page_type select ville/departement/region (pageType), pays select FR/CH/BE,
ville, departement, departement_nom (departementNom), departement_num (departementNum),
region, type_zone (typeZone), slug_ville (slugVille), slug_departement (slugDepartement),
slug_region (slugRegion), is_departement_page (isDepartementPage).

Médias : hero_image image (heroImage), hero_image_credit text (heroImageCredit),
hero_image_source url (heroImageSource). `heroImage` sert de hero front et d'OG fallback
si le champ SEO `ogImage` est vide.

Maillage : villes_departement repeater (nom, slug, type_zone), villes_proches repeater (nom),
maillage repeater (nom, slug, type_zone), maillage_scope (maillageScope).

Climat : climat_description (climatDescription), zone_climatique (zoneClimatique),
zone_climatique_cee (zoneClimatiqueCee), description_climatique (descriptionClimatique),
jours_sup_30c, temp_moy_estivale, pic_historique, jours_sup_30c_10ans,
temp_moy_estivale_10ans, jours_sup_30c_20ans, temp_moy_estivale_20ans,
reduction_sous_toiture.

Contenu : intro_section group (hook, accroche, kpis value/label, contexte, cout_cache,
trust_line, urgence_line), content group (activites_intro, secteurs label/image/stat/description,
probleme/solution/preuve fields), seo_content group (h2, intro, sections h3/content).

### `ficheFormulaireHubspot` (group `group_cvb_fiche_formulaire_hubspot`, sur formulaire_hubspot, graphql `ficheFormulaireHubspot`)
code text, portal_id text (portalId), form_guid text (formGuid), mode select api/embed,
champs repeater : cle_locale text (cleLocale), label text, nom_interne text (nomInterne),
object_type_id select 0-1/0-2 (objectTypeId), actif true_false, requis true_false.
Le champ relationship `formulaire_hubspot` des layouts conserve `hubspot_form_id` en fallback legacy.

## Options pages (graphql `optionsSite`, slug admin `options-site`, capability edit_posts)

Onglets (tabs ACF) :
1. **Navigation** : menu_principal repeater (label text, lien text, sous_items repeater (label, lien, description text)) (menuPrincipal), cta_navbar clone_cta (ctaNavbar).
2. **Footer** : colonnes_footer repeater (titre text, liens repeater (label, lien)) (colonnesFooter), texte_legal textarea (texteLegal), reseaux_sociaux repeater (reseau select: linkedin/youtube/instagram/x, url url) (reseauxSociaux).
3. **Coordonnées** : telephone text, email text, adresse textarea, horaires text.
4. **CTA globaux** : sticky_cta_label text (stickyCtaLabel), sticky_cta_lien text (stickyCtaLien), reassurances_defaut repeater (texte text) (reassurancesDefaut).
5. **Preuves** : logos_clients repeater (image image, nom text) (logosClients), logos_presse repeater (image image, nom text, lien text) (logosPresse), chiffres_cles repeater (value, label) (chiffresCles).
6. **SEO défaut** : og_image_defaut image (ogImageDefaut), suffixe_title text (suffixeTitle).

## ⚠ Piège WPGraphQL for ACF : noms de sous-champs partagés

Les types GraphQL des repeaters/groups imbriqués sont nommés `{Prefix}{NomDuChamp}` SANS le
layout parent : deux layouts qui partagent un nom de champ imbriqué partagent le MÊME type.
Règle : un nom de champ imbriqué réutilisé entre layouts doit avoir des sous-champs IDENTIQUES
(ex. `entete`, `cta`) — sinon suffixer le nom par le layout (`lignes_comparatif`,
`lignes_situations`, `lignes_specs`, `colonnes_specs`, `items_certifications`).

## Icônes lucide autorisées (select `icone`)

thermometer-sun, thermometer-snowflake, droplets, shield-check, clock, euro, leaf, factory,
warehouse, store, building-2, school, tractor, sun, snowflake, wrench, hard-hat, check-circle-2,
trending-down, trending-up, zap, wind, cloud-sun, gauge, ruler, paint-roller, spray-can, map-pin,
phone, file-text, award, users
