# Schéma JSON — documentation de référence

Documente `client-json-structure.json` (gabarit vide, source de toute
création de client) et sa version remplie, `{prenom-nom}.json` (ex.
`client-json-exemple.json`). Ne pas confondre avec le balisage schema.org
(`LocalBusiness`/`Person`/`FAQPage`), généré par le template à partir
de ces données — deux choses différentes malgré le nom proche.

Toute clé absente ou `null` doit être traitée par le template comme
"non renseignée", jamais comme une erreur. Sérialisation toujours en
`JSON_PRETTY_PRINT`, clés triées alphabétiquement.

**Convention générale de valeur vide** : absence de clé, ou `null` si
la clé doit rester présente pour la lisibilité du gabarit. Jamais
`""`, jamais de texte placeholder (`"Auteur anonyme"`), jamais de
booléen dédié type `prest_01_active`.

## Le couple `{slug}.json` / `{slug}.brouillon.json`

Ce document décrit le contenu d'**un** fichier — la structure est
identique pour les deux. Ce qui diffère, c'est lequel est actif à un
instant donné :
- `{slug}.brouillon.json` — seul fichier modifié par `/gestion`.
- `{slug}.json` — copie publiée, mise à jour uniquement par l'action
  "Publier" dans l'admin.

Voir `client-dossier-arborescence.md` pour le détail du mécanisme.

## Identité (obligatoire)

| Clé | Type | Description |
|---|---|---|
| `slug` | string | Identifiant stable du client. Nom de fichier dans `storage/app/tenants/` et de dossier dans `public/tenants/`. Ne change jamais. |
| `prenom` | string | Toujours séparé de `nom`, jamais fusionné — y compris dans `avis` et tout futur champ nommant une personne. |
| `nom` | string | |
| `nom_commercial` | string, **optionnel** | Si différent du nom personnel. |
| `template` | string | Slug du template à utiliser (ex. `template-2-0`), correspond à un dossier dans `resources/views/tenant-templates/`. |

## Localisation (obligatoire, un ou plusieurs cabinets)

| Clé | Type | Description |
|---|---|---|
| `metier` | objet | Voir section dédiée ci-dessous. |
| `adresses` | array d'objets `{rue, code_postal, departement, ville, pays, telephone, horaires, google_maps_embed?, google_maps_lien?}` | **Tableau, jamais un objet unique** — un client peut exercer dans plusieurs cabinets (ex. deux villes). Chaque entrée porte son propre téléphone, ses propres horaires et sa propre carte, puisque ces informations diffèrent réellement d'un cabinet à l'autre. `departement` : numéro seul (ex. `"11"`), jamais fusionné dans `ville`. **Le template affiche une carte par entrée** sur la page d'accueil, et boucle sur `adresses[]` dans le footer. Le "téléphone principal" affiché en en-tête et dans le CTA par défaut est celui de `adresses[0]` — pas de champ `telephone` dupliqué à la racine. |

## Contact & action (obligatoire sauf mention contraire)

| Clé | Type | Description |
|---|---|---|
| `email` | string, **optionnel** | Absent si le client ne souhaite pas publier d'email. |
| `lien_reservation` | string, **optionnel** | URL d'une application de prise de rendez-vous en ligne (Doctolib, Calendly, etc.), si le client en utilise une. **Quand renseigné, prend automatiquement la priorité sur `cta`** — voir règle ci-dessous. Ne jamais dupliquer cette URL dans `cta.lien`. |
| `cta` | objet `{lien, label?, icone?}` | Seul `lien` est obligatoire. `label` par défaut `"Prendre RDV"`, `icone` par défaut `"bi-calendar-date"` si absents. Remplace toutes les anciennes conventions concurrentes de l'ancien système (`cta_lien`/`cta_label`, `ent_lien_rdv`, `lien_partenaire_XX_texte/_url`). **Règle de priorité** : si `lien_reservation` est renseigné, le template l'utilise comme `lien` du bouton CTA (avec le label/icône par défaut ci-dessus), en ignorant le contenu de `cta` — c'est une décision de rendu, pas une fusion de données. `cta` ne reprend son rôle normal que si `lien_reservation` est absent. |
| `reseaux_sociaux` | objet `{instagram?, facebook?, youtube?, tiktok?, linkedin?}` | Profils **du praticien lui-même**, fixes, indépendants de la page consultée. Chaque clé est l'URL du profil correspondant, absente si le client n'a pas ce réseau. Sert uniquement à alimenter la propriété schema.org `sameAs` (uniquement les clés renseignées) — aucun affichage visuel prévu pour l'instant. **Ne pas confondre avec le bloc de partage social** (Messenger/WhatsApp/SMS) : celui-ci ne stocke aucune donnée, il partage l'URL de la page couramment consultée par le visiteur, générée par le template à la volée. |
| `domaines` | array de strings | **Peut être vide** avant le lancement (client encore en construction, prévisualisé via `/sandbox/{slug}`). |
| `publie` | boolean | Contrôle l'indexation Google du domaine réel (`X-Robots-Tag` + meta `noindex` si `false`), indépendamment du mécanisme brouillon/publié. `false` par défaut à la création. `/sandbox/*` reste `noindex` systématiquement, quelle que soit sa valeur. |

## Identifiants légaux (optionnel)

| Clé | Type | Description |
|---|---|---|
| `siret` | string, **optionnel** | |
| `tva` | string, **optionnel** | |

## Champs optionnels liés à la verticale métier

| Clé | Type | Description |
|---|---|---|
| `adeli` | string, **optionnel** | Uniquement pertinent pour les professions de santé réglementées. Champ présent dans le gabarit (valeur `null` par défaut) mais **non affiché par le template si `null`** — pas besoin de le retirer du schéma selon le métier, le template gère l'affichage conditionnel. |
| `rpps` | string, **optionnel** | Idem. |

## SEO (obligatoire)

| Clé | Type | Description |
|---|---|---|
| `meta_accueil` | objet `{titre, description, mots_cles}` | Title, meta description et mots-clés de la page d'accueil. Pas de champ `canonical` stocké : calculé par Laravel à partir de la route, jamais dupliqué en donnée. |
| `google_site_verification` | string ou `null` | Hash du fichier `google{hash}.html` (placé à la racine de `public/`, voir `client-dossier-arborescence.md`) si le client est vérifié dans Search Console. `null` si non vérifié. |
| `ville_seo` | string, **optionnel** | Ville **cible** pour le référencement local, distincte de `adresses[0].ville`. Une seule stratégie SEO par site, même si plusieurs cabinets existent — pas de `ville_seo` par adresse. Utile quand le client habite un village sans volume de recherche propre, mais souhaite être trouvé pour la ville voisine la plus recherchée (ex. `adresses[0].ville: "Taurize"`, `ville_seo: "carcassonne"`). Sert au futur annuaire `/ville/metier/` et au nom de fichier image (voir `client-dossier-arborescence.md`). Si absent, retombe sur `adresses[0].ville`. **⚠️ Ne doit jamais remplacer les villes réelles dans le balisage schema.org `LocalBusiness`** — Google recoupe l'adresse structurée avec la fiche Google Business Profile (cohérence NAP), y placer une ville différente de l'adresse réelle serait interprété comme du spam local. |
| `llm_txt` | boolean | Génère un `llm.txt` pour ce domaine si `true`. |
| `llm_full_txt` | boolean | Génère un `llm-full.txt` pour ce domaine si `true`. |

## Photos d'accueil (obligatoire, structure toujours présente)

| Clé | Type | Description |
|---|---|---|
| `photos_accueil` | array de `{image, alt}` | **Ordre significatif** : position 1 = photo hero, 2 = illustration biographie, 3 = illustration approche métier. Le nom de fichier (`image`) est généré une seule fois à l'upload et jamais recalculé — voir `client-dossier-arborescence.md`. `alt` librement modifiable sans toucher au fichier. |

## `metier` (obligatoire)

| Clé | Type | Description |
|---|---|---|
| `metier.slug` | string | Ex. `"ferronnier-metallier"`. Sert aussi de composant du nom de fichier image (voir `client-dossier-arborescence.md`) et prépare un futur annuaire `/ville/metier/`, non construit actuellement. |
| `metier.libelle` | string | Ex. `"Ferronnier d'art"`. Affichage uniquement, jamais utilisé dans un nom de fichier ou une URL. |
| `metier.specialites` | array de strings, **optionnel** | Longueur libre, remplace les anciennes clés `ent_specialite_1..N`. |
| `metier.description`, `.caracteristiques`, `.avantages`, `.benefices` | objets `{titre, texte}`, **optionnels** | Blocs de présentation du métier. |
| `metier.citation` | objet `{texte, auteur?}`, **optionnel** | Même structure que la citation d'une prestation, pour ne pas perdre l'attribution d'une citation citée. `auteur` optionnel — jamais de placeholder si inconnu. |

## Contenu narratif (optionnel)

| Clé | Type | Description |
|---|---|---|
| `biographie` | objet `{intro, items[]}` | `items` est un vrai tableau d'objets `{titre, texte}`, longueur libre. |
| `approche` | objet `{intro?, vision?, mission?, valeurs?}` | Chaque sous-champ `{titre, texte}` est indépendamment optionnel. |

## Prestations (obligatoire, tableau vide accepté)

`prestations` est un tableau d'objets "prestation" — **le même type
d'objet** est réutilisé à l'intérieur de son propre champ
`pages_liees`. Un seul schéma, un seul template Blade pour l'afficher,
qu'il s'agisse d'une prestation principale, d'une page liée, ou d'un
item de portfolio (qui a sa propre structure plus légère, voir
ci-dessous).

| Clé (dans un objet prestation) | Type | Description |
|---|---|---|
| `nom` | string | Obligatoire. |
| `slug` | string | Obligatoire. **Figé dès la création, jamais renommé** — sert aussi de nom de dossier portfolio (`public/tenants/{slug-client}/assets/img/portfolio/{slug-prestation}/`). Renommer une prestation implique de la supprimer et d'en recréer une nouvelle. |
| `descriptif_court` | string | |
| `tarif` | string, **optionnel** | |
| `duree` | string, **optionnel** | |
| `public_concerne` | string, **optionnel** | |
| `lieu_intervention` | string, **optionnel** | |
| `indications`, `accroche`, `deroulement`, `avantages`, `benefices`, `declencheur` | objets `{titre, texte}`, **optionnels** | Blocs de contenu de la page prestation. |
| `citation` | objet `{texte, auteur?}`, **optionnel** | `auteur` lui-même optionnel — jamais de placeholder type `"Auteur anonyme"`, absent si inconnu. **Repli automatique** : si absent, le template affiche `metier.citation` à la place plutôt que de ne rien afficher. Comportement à reproduire (existait dans l'ancien système), pas une simple absence de contenu. |
| `faq` | array de `{question, reponse}`, **optionnel** | FAQ propre à cette prestation, distincte de la FAQ globale de la page d'accueil (voir plus bas). |
| `meta` | objet `{titre, description, mots_cles}`, **optionnel** | Si absent, le template génère un titre par défaut à partir de `nom`. |
| `portfolio_intro` | objet `{titre, texte}`, **optionnel** | Titre et texte d'introduction de la section portfolio — contenu éditorial à valeur marketing, indépendant de la visibilité de la section (voir ci-dessous). |
| `portfolio` | array d'objets, **optionnel** | Voir section dédiée ci-dessous. |
| `pages_liees` | array d'objets prestation, **optionnel** | Mêmes champs qu'une prestation normale, **mais ne doit pas contenir son propre `pages_liees`** (pas de nesting à plus d'un niveau) — invisible du menu principal, avec lien retour automatique vers sa prestation parente. |

### Portfolio — imbriqué dans la prestation, jamais déduit du système de fichiers

```json
"portfolio_intro": { "titre": null, "texte": null },
"portfolio": [
  {
    "image": "chemin généré à l'upload, figé",
    "alt": null,
    "titre": null,
    "categorie": null,
    "description": null,
    "tarif": null
  }
]
```

- **Visibilité de la section pilotée uniquement par `portfolio` non
  vide** — `portfolio_intro` ne déclenche rien à lui seul (contrairement
  à l'ancien système, où le titre commandait l'affichage). Un
  `portfolio_intro` renseigné sans aucun item ne doit rien afficher.
- Alimenté **uniquement** via `/gestion` (upload d'image + saisie des
  métadonnées dans le même geste) — jamais par scan du dossier
  physique. Un nom de fichier ou un chemin ne peut pas porter un
  titre, une description, un tarif ou une catégorie.
- Les boutons de filtre Isotope se déduisent dynamiquement des
  valeurs de `categorie` présentes dans ce tableau — aucune liste de
  catégories déclarée séparément.
- Isotope ne se charge que sur les pages où un `portfolio` non vide
  existe.
- Un item = une seule image (pas de galerie multi-image par item).
- `image` est un chemin figé à l'upload, indépendant de `titre` (voir
  `client-dossier-arborescence.md` pour la règle de nommage).

## FAQ page d'accueil (optionnel)

| Clé | Type | Description |
|---|---|---|
| `faq` | array de `{question, reponse}`, **optionnel** | FAQ de la page d'accueil. Distincte de la FAQ par prestation (`prestations[].faq`) — les deux niveaux coexistent intentionnellement. |

## Avis (optionnel)

| Clé | Type | Description |
|---|---|---|
| `avis` | array de `{prenom, nom, qualification?, texte, note?}`, **optionnel** | `prenom`/`nom` séparés, comme partout ailleurs dans le schéma. `qualification` (ex. `"Cavalière élite pro"`, `"Pratiquant de Judo"`) qualifie l'auteur de l'avis pour en renforcer la crédibilité — optionnel. `note` (1 à 5) optionnelle. |

## Partenaires (optionnel)

| Clé | Type | Description |
|---|---|---|
| `liens_partenaires` | array de `{label, url}`, **optionnel** | |

## Crédits (optionnel)

| Clé | Type | Description |
|---|---|---|
| `credits` | objet `{interviewer, photographe, graphiste}`, chacun `{nom, lien}`, **optionnel** | |

## Rendu HTML brut vs échappé — obligatoire à respecter dans les vues Blade

Certains champs contiennent volontairement du HTML inline (`<b>`,
`<br>`, `<i>`, `<strong>`) hérité de l'ancien système et doivent être
rendus **sans échappement** (`{!! $valeur !!}`). D'autres doivent
**toujours** être échappés (`{{ $valeur }}`), notamment tout ce qui
finit dans un attribut HTML (`alt`, `href`), une balise `<meta>`, ou
un `<title>` — y injecter du HTML y serait invalide ou cassé
visuellement.

**⚠️ Deux noms de champ existent à deux niveaux différents avec un
traitement opposé — la règle doit être appliquée par chemin complet,
jamais par simple nom de clé :**
- `prestations[].tarif` → **brut** (texte long, peut contenir
  `<b>`/`<br>`) — mais `prestations[].portfolio[].tarif` → **échappé**
  (juste un prix court, ex. `"590€"`).
- `meta.description` / `prestations[].meta.description` → **échappé**
  (balise `<meta>`) — mais `prestations[].portfolio[].description` →
  **échappé également** (texte affiché, mais sans HTML dans les
  données migrées à ce jour — à garder échappé sauf besoin futur
  explicite).

### Rendu brut (`{!! !!}`)

`adresses[].horaires` ·
`metier.description.texte` · `metier.caracteristiques.texte` ·
`metier.avantages.texte` · `metier.benefices.texte` ·
`metier.citation.texte` ·
`biographie.intro` · `biographie.items[].texte` ·
`approche.intro` · `approche.vision.texte` · `approche.mission.texte` ·
`approche.valeurs.texte` ·
`prestations[].tarif` · `prestations[].duree` ·
`prestations[].public_concerne` · `prestations[].lieu_intervention` ·
`prestations[].indications.texte` · `prestations[].accroche.texte` ·
`prestations[].deroulement.texte` · `prestations[].avantages.texte` ·
`prestations[].benefices.texte` · `prestations[].declencheur.texte` ·
`prestations[].citation.texte` · `prestations[].portfolio_intro.texte` ·
`prestations[].faq[].reponse` ·
`faq[].reponse` ·
`avis[].qualification` · `avis[].texte`

(règle identique pour `prestations[].pages_liees[]`, qui partage le
même schéma qu'une prestation)

### Toujours échappé (`{{ }}`)

`prenom` · `nom` · `nom_commercial` ·
`adresses[].rue` · `adresses[].code_postal` · `adresses[].departement` ·
`adresses[].ville` · `adresses[].pays` · `adresses[].telephone` ·
`adresses[].google_maps_embed` · `adresses[].google_maps_lien` ·
`email` ·
`lien_reservation` ·
`cta.lien` · `cta.label` · `cta.icone` ·
`reseaux_sociaux.instagram` · `reseaux_sociaux.facebook` ·
`reseaux_sociaux.youtube` · `reseaux_sociaux.tiktok` ·
`reseaux_sociaux.linkedin` ·
`siret` · `tva` · `adeli` ·
`rpps` · `domaines[]` · `google_site_verification` · `ville_seo` · `slug` ·
`template` ·
`photos_accueil[].image` · `photos_accueil[].alt` ·
`metier.slug` · `metier.libelle` · `metier.specialites[]` ·
`metier.citation.auteur` ·
`metier.description.titre` · `metier.caracteristiques.titre` ·
`metier.avantages.titre` · `metier.benefices.titre` ·
`biographie.items[].titre` ·
`approche.vision.titre` · `approche.mission.titre` ·
`approche.valeurs.titre` ·
`prestations[].nom` · `prestations[].slug` ·
`prestations[].descriptif_court` · `prestations[].citation.auteur` ·
`prestations[].indications.titre` · `prestations[].accroche.titre` ·
`prestations[].deroulement.titre` · `prestations[].avantages.titre` ·
`prestations[].benefices.titre` · `prestations[].declencheur.titre` ·
`prestations[].faq[].question` · `prestations[].meta.titre` ·
`prestations[].meta.description` · `prestations[].meta.mots_cles` ·
`prestations[].portfolio_intro.titre` ·
`prestations[].portfolio[].image` · `.alt` · `.titre` · `.categorie` ·
`.description` · `.tarif` ·
`faq[].question` ·
`avis[].prenom` · `avis[].nom` ·
`liens_partenaires[].label` · `liens_partenaires[].url` ·
`credits.interviewer.nom` · `credits.interviewer.lien` ·
`credits.photographe.nom` · `credits.photographe.lien` ·
`credits.graphiste.nom` · `credits.graphiste.lien` ·
`meta_accueil.titre` · `meta_accueil.description` ·
`meta_accueil.mots_cles`
