# Cahier des charges fonctionnel — lesiteduliberal.fr multi-tenant

Liste les capacités attendues de l'application, indépendamment de
l'implémentation Laravel qui les réalise. Sert d'ancrage pour vérifier
que chaque nouvelle décision correspond à une capacité listée ici,
plutôt que de dériver hors scope sans s'en rendre compte.

**Destinataire de l'application : l'agence (lesiteduliberal.fr), pas
le client final.** L'artisan ou l'indépendant dont le site est généré
n'a et n'aura jamais accès à `/gestion`, sous quelque forme que ce
soit — pas de portail client, pas de mode d'édition allégé. C'est un
principe produit, pas seulement une conséquence de "un seul compte
admin, pas de mot de passe par tenant" (section 2).

Complète `client-json-explication.md` (contenu du JSON) et
`client-dossier-arborescence.md` (arborescence physique d'un client).
Ce document ne décrit **pas** l'arborescence de code Laravel
(contrôleurs, vues, dossiers) — c'est au développeur (Claude Code) de
la proposer, à condition qu'elle respecte les contraintes de la
section 0.

**Statut** : `MVP` (premier test, jerome-loze) · `V2` (après
validation du MVP) · `Hors scope` (explicitement exclu, ne pas
construire même en option).

---

## 0. Contraintes techniques transverses (à respecter quelle que soit l'implémentation)

Ces règles s'imposent au développeur indépendamment des choix de code
qu'il fait par ailleurs — ce sont des exigences de chef de projet, pas
des suggestions d'architecture.

- **Zéro base de données pour le contenu client.** Chaque client est
  un fichier JSON plat + un dossier d'assets. Aucune table, aucune
  migration pour ce contenu.
- **Un client n'est créé que par import d'un fichier JSON** via
  `/gestion`. Aucun autre chemin de création (pas de commande de
  scaffolding, pas de formulaire "nouveau client" vide).
- **Brouillon/publié à sens unique.** `/gestion` n'écrit jamais que
  dans le fichier brouillon d'un client. Une action "Publier"
  explicite copie le brouillon vers le fichier public. Le brouillon
  n'est jamais supprimé après publication.
- **Deux points d'entrée de lecture** : le domaine réel du client (lit
  la version publiée) et une URL de prévisualisation sur
  `lesiteduliberal.fr` (lit la version brouillon). Même mécanisme pour
  un client pas encore lancé et pour prévisualiser une modification
  sur un client déjà en ligne.
- **Un client peut avoir plusieurs domaines.** Le mapping domaine →
  client vit dans les données du client lui-même, jamais dans un
  fichier de configuration séparé.
- **Indexation Google contrôlée par une donnée, pas par l'URL.** Un
  client non "publié" est en `noindex` sur son domaine réel. La zone
  de prévisualisation reste `noindex` dans tous les cas, sans
  exception.
- **Un seul compte administrateur**, avec accès à tous les clients.
  Pas de mot de passe par client. Protection contre les tentatives de
  connexion automatisées (throttling).
- **Portfolio jamais déduit du système de fichiers.** Les métadonnées
  d'un item de portfolio (titre, catégorie, description, tarif) sont
  saisies via l'admin en même temps que l'upload de l'image — jamais
  inférées d'un nom de fichier ou d'un scan de dossier.
- **Continuité SEO obligatoire pour les clients déjà en ligne.**
  Sitemap et fichier robots générés dynamiquement par domaine à partir
  des données publiées. Fichier de vérification Search Console
  conservé. Aucune régression de référencement au moment de la
  migration d'un client déjà indexé.
- **Le template d'affichage est découplé des données.** Chaque client
  déclare quel template il utilise ; un même template sert plusieurs
  clients. Aucune donnée métier ne doit forcer un template spécifique
  à exister.
- **Pas de package de modularité Laravel** (type `nwidart/laravel-modules`)
  — le MVP couvre déjà l'ensemble du périmètre, un découpage par
  module ajouterait de la complexité sans bénéfice pour un projet de
  cette taille porté par un développeur seul.
- **Import JSON strictement validé contre le schéma.** Tout écart au
  schéma (`client-json-explication.md`) est signalé clairement à
  l'import — jamais deviné, jamais corrigé silencieusement, jamais
  accepté partiellement.
- **`publie` forcé à `false` côté serveur à l'import**, indépendamment
  de ce que contient le fichier JSON importé — aucune publication
  accidentelle possible par erreur de configuration en amont.
- **Affichage conditionnel systématique.** Chaque section du template
  ne s'affiche que si les données correspondantes sont renseignées —
  jamais d'erreur, jamais de placeholder visible pour un champ
  optionnel absent.
- **Protection CSRF sur tous les formulaires d'administration** — non
  négociable, compte tenu de la faille de l'ancien système (admin
  sans authentification ni protection).

## 1. Consultation publique du site client

| Capacité | Statut |
|---|---|
| Page d'accueil (identité, métier, biographie, approche, prestations, avis, FAQ) | MVP |
| Page prestation (contenu détaillé, FAQ dédiée) | MVP |
| Page liée (sous-page de prestation, invisible du menu) | MVP |
| Portfolio par prestation avec filtres par catégorie (Isotope) | MVP |
| Sitemap.xml dynamique par domaine | MVP |
| robots.txt dynamique par domaine | MVP |
| Balisage schema.org (LocalBusiness / Person / FAQPage) | MVP |
| Propriété `hasMap` (schema.org) alimentée par `google_maps_lien` | MVP |
| Plusieurs cabinets par client (`adresses[]`), une carte par entrée sur l'accueil et dans le footer | MVP |
| Bloc de partage social (Messenger/WhatsApp/SMS) sur la page courante | MVP |
| Propriété `sameAs` (schema.org) alimentée par `reseaux_sociaux` | MVP |
| CTA automatiquement piloté par `lien_reservation` s'il existe | MVP |
| Notation dynamique des avis (`avis[].note`, étoiles) et `avis[].qualification` affichée | MVP |
| `noindex` conditionnel (`publie: false`, et systématique sur la zone de prévisualisation) | MVP |
| Fichier de vérification Google Search Console | MVP |
| `llm.txt` / `llm-full.txt` par domaine | MVP |
| Annuaire public `/ville/metier/` | Hors scope |

## 2. Administration

| Capacité | Statut |
|---|---|
| Authentification (compte unique, throttling de connexion) | MVP |
| Import d'un fichier JSON → création d'un client | MVP |
| Refus d'import si le `slug` existe déjà | MVP |
| Édition par formulaire de tous les champs texte | MVP |
| Upload des 3 photos d'accueil (ordonnées : hero / bio / approche) | MVP |
| Upload + saisie métadonnées portfolio (image + titre + catégorie + description + tarif) | MVP |
| Action "Publier" (copie brouillon → publié, sans suppression du brouillon) | MVP |
| Aperçu de la version brouillon avant publication | MVP |
| Indication du nombre de caractères sur les champs SEO (label du champ, non bloquant) | MVP |
| Re-synchronisation d'un client existant par nouvel import JSON | Hors scope |
| Mot de passe par tenant | Hors scope (décision fermée) |

## 3. Multi-domaine / routage

| Capacité | Statut |
|---|---|
| Résolution par domaine réel (données publiées) | MVP |
| Résolution par slug en prévisualisation (données brouillon) | MVP |
| 404 propre si aucun tenant ne correspond | MVP |
| Plusieurs domaines par client | MVP |

## 4. Rendu du template — décisions actées suite à l'audit de `cms_portfolio`

Issu de `template-analyse.md`. Les correspondances de champs détaillées
restent dans ce document ; ce qui suit ne reprend que les décisions de
comportement à appliquer lors de la construction Blade.

| Décision | Statut |
|---|---|
| Balisage `openingHoursSpecification` (schema.org) | Omis — `horaires` est un texte libre, pas assez structuré pour être fiable, mieux vaut l'absence qu'une fausse donnée structurée |
| `acceptsReservations: true` (schema.org) | Codé en dur, vrai pour tous les clients actuels, pas de champ dédié |
| `FAQPage.mainEntity` (JSON-LD) | Généré depuis la totalité de `faq[]`, sans plafond (l'ancien système plafonnait à 10, incohérent avec l'affichage visible non plafonné) |
| Obfuscation de l'email (anti-scraping) | Comportement à reproduire à l'identique (encodage + décodage au clic), pas un simple `mailto:` en clair |
| Repli citation prestation → citation globale | Comportement à reproduire (voir `client-json-explication.md`, section prestations) |
| Portfolio : visibilité de section | Pilotée uniquement par `portfolio` non vide — `portfolio_intro` n'affiche rien à lui seul (changement de règle par rapport à l'ancien système) |
| Bloc de partage social (Messenger/WhatsApp/SMS) | Markup mort dans l'ancien système, à réactiver. Aucune donnée stockée : partage l'URL de la page couramment consultée, générée par le template — jamais une donnée de `reseaux_sociaux` |
| Avis : étoiles et qualification | Construction neuve, rien à reproduire de l'ancien système (étoiles codées en dur à 5/5, `qualification` inexistant) — `avis[].note` pilote des étoiles dynamiques, `avis[].qualification` s'affiche sous le nom de l'auteur |
| Bibliothèques front à conserver | Bootstrap, AOS, GLightbox, Swiper (témoignages), Isotope + imagesloaded (portfolio uniquement, jamais chargées si le portfolio de la page est vide) |

## 5. Hors scope (rappel groupé, déjà acté dans les autres documents)

- Suivi de prestation (dossiers `K7mP2x` etc.)
- Console centrale / Face 2
- Documents PDF et vidéos par client (dossiers réservés mais vides)
- Conversion automatique d'image (seul `.webp` accepté à l'upload)
- Portail client / tout accès de l'artisan à `/gestion`, sous quelque
  forme que ce soit — l'agence est la seule destinataire de
  l'application (voir en-tête de ce document)
