# CMS maison

> Phase d'audit — **conception proposée**. Aucune table n'est créée à ce stade ; le schéma ci-dessous
> est une **proposition** à matérialiser dans `database/schema.sql` lors d'une phase ultérieure validée.

## 1. Objectif et principe

Remplacer le mécanisme actuel (« dépôt FTP dans `image_in/` + exécution de `update.php` ») par un
**back-office web** où l'administrateur gère galeries, images, accueil, contact et SEO **sans toucher
au système de fichiers ni relancer de régénération globale**.

Renversement clé par rapport à l'ancien système :

| Ancien (filesystem-driven)                          | Nouveau (CMS, base = source de vérité)            |
|-----------------------------------------------------|---------------------------------------------------|
| Métadonnées encodées dans les noms de fichiers (`##`, `++`, `--`) | Champs structurés en base (nom, ordre, tri, alt) |
| Tri par date EXIF ou nom de fichier                 | **Ordre manuel** explicite (glisser-déposer)      |
| `text.txt` édité à la main                          | Éditeur HTML dans le back-office                  |
| `update.php` régénère tout (destructif)             | Chaque action est unitaire et non destructive     |
| Pas d'authentification réelle (Basic Auth /libs)    | Auth admin `password_hash` + sessions + CSRF      |

## 2. Périmètre fonctionnel (exigences AGENTS.md)

### 2.1 Gestion des galeries
- Créer / modifier / désactiver / supprimer (suppression seulement après confirmation).
- **Nom obligatoire**, slug (URL propre) unique, modifiable.
- **Ordre d'affichage** des galeries (manuel).
- Statut **brouillon / publié**.
- **Texte HTML** de présentation (intro et/ou corps) via éditeur.
- **Image de couverture** (choisie parmi les images de la galerie ou dédiée).
- **Galeries plates (mono-niveau)** : pas d'imbrication. **Tags en attente** (piste possible plus tard,
  D-23), non implémentés pour l'instant.

### 2.2 Gestion des images / items
- Upload (un ou plusieurs fichiers).
- **Nom obligatoire** pour chaque image.
- Texte alternatif (`alt`) — distinct du nom si besoin pour le SEO/accessibilité.
- **Ordre manuel** (réorganisation par glisser-déposer).
- Statut **publié / masqué**.
- Miniature générée automatiquement (+ version « cover » et version « web »).
- Modification, suppression ou désactivation.
- Optimisation raisonnable (qualité JPEG, dimensions max).

### 2.3 Page d'accueil
- Titre principal, texte principal, blocs de contenu, images éventuelles.
- Liste des galeries publiées (comme en production).
- Champs SEO propres.

### 2.4 Formulaire de contact
- **Activable / désactivable** depuis le CMS.
- Si activé : affichage public, champs nécessaires, validation serveur, protection anti-abus simple.
- **Email de destination en configuration non versionnée** (`config/local.php`).
- Si désactivé : non affiché (et endpoint inactif).

### 2.5 SEO (par page et par galerie)
- `title`, `meta description`, `slug`, `H1`, indexable / non indexable.
- Génération de `sitemap.xml`, `robots.txt` propre, balises Open Graph, 404 réelle, redirections utiles.

### 2.6 Apparence / couleurs du site (thème)
- Écran d'admin **simple** pour régler **quelques couleurs** du site (fond, texte, accent/liens,
  fond d'en-tête…). Voir §11. *(Exigence client ; décision D-13.)*

### 2.7 Navigation (menu responsive avec hamburger)
- Menu de navigation public, **horizontal en grand écran** et **replié en hamburger** sur mobile,
  alimenté par les galeries publiées + Accueil + Contact. Voir §12. *(Exigence client ; décision D-12.)*

### 2.8 Chargement progressif des galeries (lazy-load) paramétrable
- Le rendu des galeries charge les images **progressivement au défilement**, avec un **paramètre
  d'activation/réglage dans l'admin**. Voir §13. *(Exigence client ; décision D-14.)*

## 3. Proposition de schéma de données (NON définitif)

> Encodage **utf8mb4** / collation `utf8mb4_unicode_ci`. Clés primaires explicites, index utiles,
> compatibilité **MySQL 8 / MariaDB 10.11** (éviter les spécificités MySQL non portables).
> Présenté en pseudo-DDL **illustratif** — à finaliser et placer dans `database/schema.sql` plus tard.

### 3.1 `admins` — comptes d'administration
| Colonne          | Type / note                                  |
|------------------|----------------------------------------------|
| `id`             | PK, INT AUTO_INCREMENT                        |
| `email`          | identifiant de connexion, UNIQUE             |
| `password_hash`  | VARCHAR(255), via `password_hash()`          |
| `display_name`   | nom affiché                                  |
| `last_login_at`  | DATETIME NULL                                |
| `created_at`     | DATETIME                                     |

### 3.2 `galleries` — galeries (avec imbrication prévue)
| Colonne            | Type / note                                                  |
|--------------------|--------------------------------------------------------------|
| `id`               | PK                                                           |
| `parent_id`        | **non utilisé** (galeries plates, pas d'imbrication — D-23)        |
| `slug`             | VARCHAR, UNIQUE (URL propre)                                |
| `title`            | nom affiché (**obligatoire**)                               |
| `intro_html`       | TEXT — texte de présentation (HTML filtré)                  |
| `body_html`        | TEXT NULL — contenu complémentaire éventuel                 |
| `cover_image_id`   | INT NULL → `gallery_images.id`                              |
| `sort_order`       | INT — ordre d'affichage manuel des galeries                |
| `status`           | ENUM('draft','published') (ou TINYINT)                     |
| `seo_title`        | VARCHAR NULL                                                |
| `seo_description`  | VARCHAR NULL                                                |
| `seo_noindex`      | TINYINT(1) DEFAULT 0                                        |
| `created_at` / `updated_at` | DATETIME                                          |

### 3.3 `gallery_images` — images / items
| Colonne            | Type / note                                                  |
|--------------------|--------------------------------------------------------------|
| `id`               | PK                                                           |
| `gallery_id`       | INT → `galleries.id` (ON DELETE CASCADE ou restriction)     |
| `name`             | VARCHAR **NOT NULL** (nom obligatoire)                      |
| `alt`              | VARCHAR NULL (texte alternatif ; défaut = `name`)          |
| `filename`         | VARCHAR — nom du binaire stocké (normalisé)                |
| `original_name`    | VARCHAR — nom d'origine à l'upload (traçabilité)           |
| `width` / `height` | INT NULL — dimensions de l'original (cache)                |
| `sort_order`       | INT — **ordre manuel** dans la galerie                     |
| `status`           | ENUM('published','hidden') (ou TINYINT)                    |
| `created_at`       | DATETIME                                                    |

### 3.4 `pages` — pages éditoriales (accueil, etc.)
| Colonne            | Type / note                                                  |
|--------------------|--------------------------------------------------------------|
| `id`               | PK                                                           |
| `slug`             | VARCHAR UNIQUE (`home` pour l'accueil)                      |
| `title`            | VARCHAR                                                     |
| `body_html`        | TEXT (contenu HTML filtré)                                  |
| `status`           | ENUM('draft','published')                                   |
| `seo_title` / `seo_description` / `seo_noindex` | comme galeries               |
| `updated_at`       | DATETIME                                                    |

> Pages prévues : `home` (accueil) et **`mentions-legales`** (voir §14 ; activable via son `status`).

### 3.5 `settings` — paramètres du site (clé/valeur)
| Colonne   | Type / note                                                          |
|-----------|----------------------------------------------------------------------|
| `key`     | VARCHAR PK (ex. `site_title`, `tagline`, `contact_enabled`, `logo`)  |
| `value`   | TEXT                                                                |

Exemples de clés : `site_title` = `DANIEL TIZIANI`, `tagline` = `Simple, fort & juste`,
`contact_enabled` = `0/1`, `default_seo_description`, `default_lang` = `fr`/`en`, `image_author` (auteur EXIF/IPTC).

Clés ajoutées pour les exigences client (thème, menu, lazy-load) :
- **Thème** (§11) : `theme_color_bg`, `theme_color_text`, `theme_color_accent`, `theme_color_header_bg`,
  `theme_color_footer_bg`, `theme_color_hover` (6 couleurs).
- **Menu** (§12) : `menu_breakpoint` (ex. `lg`), `menu_show_contact` (0/1), `menu_mode` (`offcanvas` par défaut / `collapse`).
- **Lazy-load** (§13) : `gallery_lazyload_enabled` (0/1) et `gallery_batch_size` (nombre d'images
  par lot, déf. 12).

> L'**email de contact** n'est **pas** ici : il vit dans `config/local.php` (non versionné).

### 3.6 Tables optionnelles (selon besoin)
- `image_profiles` (`key`, `label`, `width`, `height`, `crop`, `orientation`, `webp`) — **profils de
  dérivés** configurables en admin (D-26) ; alternative plus légère : une clé `settings` au format JSON.
- `tags` (`id`, `name`, `slug`) + `gallery_tags` (`gallery_id`, `tag_id`) — **en attente** : à n'ajouter
  que si un regroupement par étiquettes devient nécessaire (D-23 ; non implémenté au lancement).
- `redirects` (`id`, `old_path`, `new_path`, `code` 301/410) — redirections SEO.
- `contact_messages` (`id`, `name`, `email`, `message`, `created_at`, `ip`) — journal des messages si l'on
  souhaite conserver une trace en plus de l'email.
- `activity_logs` (`id`, `admin_id`, `action`, `entity`, `created_at`) — historique simple.

> **Aucune** de ces tables n'est créée maintenant. Décisions sur ENUM vs TINYINT, cascades et index :
> voir `decisions.md`.

## 4. Gestion des galeries (parcours back-office)

1. **Liste** des galeries avec statut, nombre d'images, ordre (réordonnable).
2. **Création** : saisie du titre (obligatoire) → slug proposé automatiquement (modifiable) →
   statut brouillon par défaut.
3. **Édition** : intro HTML (éditeur), choix de la couverture, champs SEO, position.
4. **Publication / dépublication** (bascule statut).
5. **Suppression** : confirmation explicite ; gérer le sort des images rattachées et des dérivés.

## 5. Gestion des images

1. **Upload** dans une galerie : génération des **dérivés** à la volée (voir §6), et non plus par
   reconstruction globale.
2. Chaque image : **nom obligatoire** (rejet si vide — corrige les cas `010##.JPG` de l'ancien site),
   `alt` optionnel, statut, position.
3. **Réordonnancement** par glisser-déposer (met à jour `sort_order`).
4. **Suppression / masquage** unitaire, sans impact sur le reste de la galerie.
5. Définition de l'image comme **couverture** de la galerie en un clic.

## 6. Gestion des fichiers images (traitement)

Reproduire les tailles utiles de l'ancien site, mais **à l'upload** (pas en lot destructif) :

| Type (profil)   | Usage                     | Défaut (repris de l'existant)            | Réglable en admin            |
|-----------------|---------------------------|------------------------------------------|------------------------------|
| *(original)*    | archivage / re-génération | conservé dans `storage/uploads` (privé)  | non                          |
| **vignette**    | grille de galerie         | ~400×266 (recadrée), q~78                | dim., crop, orient., WebP    |
| **normal**      | affichage standard page   | dérivé intermédiaire (selon thème)       | dim., crop, orient., WebP    |
| **plein écran** | lightbox / grande vue     | orientation-aware : paysage ≤ ~1920px L, portrait ≤ ~1280px H, q~82 | dim., crop, orient., WebP |
| *(couverture)*  | tuile de galerie          | ~400×240 (recadrée)                      | dim., crop, orient., WebP    |

> **Profils d'image (D-26)** : chaque type expose en admin **dimensions**, **crop oui/non**,
> **gestion verticale/horizontale** et **sortie WebP oui/non** (en plus du JPEG). Stockés en base
> (`image_profiles` ou clé `settings` JSON), appliqués à l'upload et **regénérables**.

- Outil : **GD** (présent en mutualisé) encapsulé dans un `ImageService` propre — la classe
  `Zebra_Image` de l'ancien site sert de **référence d'algorithme** (modes de recadrage), pas de code à reprendre tel quel.
- **EXIF (Q-08 tranché)** : lire l'orientation pour corriger la rotation ; ne pas dépendre de la date
  EXIF pour le tri (désormais manuel). **Conserver l'EXIF** dans les dérivés et **ajouter l'auteur**
  (champ *Artist*/*Creator*) selon le paramètre admin `image_author` (D-22).
- **Upload (Q-05 tranché)** : limite **30 Mo** (`.user.ini`/`.htaccess` — à confirmer côté hébergeur,
  l'actuel plafonne à 2 Mo). À l'upload : **image « écran » orientation-aware** (paysage → largeur max ;
  portrait → hauteur max), puis **dérivés par usage** (miniature de grille, couverture de tuile…).
  **Original conservé** dans `storage/uploads` (dossier privé, hors accès web) pour re-générer.
- Entrée : JPEG/PNG. **Sortie : JPEG + WebP** (WebP activable **par type** d'image ; D-26), avec repli
  `<picture>`/JPEG côté public pour la compatibilité.

## 7. Gestion de la page d'accueil

- Éditée comme une **page** (`pages.slug = home`) : titre, texte HTML, image(s) éventuelle(s).
  **Monolingue au lancement** (langue par défaut FR ou EN, `default_lang`) mais **modèle i18n-ready**
  pour ajouter une 2ᵉ langue facilement plus tard (D-25).
- Le rendu public reprend la mise en page production : bloc texte + **liste des galeries publiées**
  (tri par `sort_order`).
- Données à reprendre depuis l'ancien `image_in/text.txt` : bio FR + EN, mention de contact.
  Le logo `daniel_tiziani.jpg` devient un asset géré.

## 8. Gestion du formulaire de contact

- Paramètre `contact_enabled` dans `settings` → affiche ou masque le formulaire **et** désactive
  le traitement serveur si off.
- Champs minimaux : nom, email, message (+ sujet éventuel).
- **Validation serveur** stricte (email valide, longueurs, champs requis).
- **Anti-abus (Q-07 tranché)** : honeypot + horodatage + limitation de fréquence, **+ Google reCAPTCHA**
  (clés en `config/local.php`, activation/configuration **plus tard**).
- **Email de destination** lu dans `config/local.php` (non versionné). Envoi via **PHPMailer** (SMTP) ;
  configuration SMTP définie plus tard (Q-06 tranché).
- Protection **CSRF** sur la soumission. Échappement de toutes les sorties.

## 9. Gestion SEO (intégrée au CMS)

- Par galerie et par page : `seo_title`, `seo_description`, `slug`, `H1` (= titre), `noindex`.
- **Corrige le défaut majeur de l'ancien site** : `<title>`/`<meta>` identiques partout, `lang="en"`
  alors que le contenu est en français.
- Génération automatique de **`sitemap.xml`** (galeries + pages publiées, non `noindex`).
- **`robots.txt`** propre (l'actuel est vide) + référence du sitemap.
- **Conserver les slugs existants** (`/anfractuosite`, `/bouches-a-clef`, …) pour éviter toute perte
  de référencement ; table `redirects` pour les exceptions.
- **404 réelle** (l'ancien site redirige tout inconnu vers l'accueil) + Open Graph de base.

## 10. Sécurité du CMS (rappel des exigences)

- `password_hash` / `password_verify`, sessions sécurisées (cookie `HttpOnly`, `SameSite`, régénération
  d'ID à la connexion), **jeton CSRF** sur tous les formulaires admin et le contact.
- **Requêtes préparées** (PDO) systématiques.
- Validation/filtrage des entrées ; **échappement** de toutes les sorties HTML.
- **Éditeur** : **TinyMCE** auto-hébergé, barre minimale (styles Normal/H1/H2/H3, gras, italique,
  exposant ; D-18). **Sanitization serveur obligatoire** à l'enregistrement (liste blanche alignée
  sur ces balises) — l'éditeur client **n'est pas** une barrière de sécurité (Q-03 tranché).
- Upload : vérifier type réel (`finfo`), extension, taille ; renommer ; stocker hors web pour l'original ;
  jamais d'exécution.
- **Aucun secret dans Git** ; `config/local.php` non versionné ; suppression de `info.php` et de tout
  endpoint de debug.

## 11. Gestion des couleurs du site (thème) — *exigence client*

Objectif : permettre de changer l'apparence **simplement**, « sans aller dans trop de détail ».

- **Admin** : un petit formulaire avec **6 sélecteurs de couleur** : fond, texte, accent/liens,
  fond d'en-tête, **fond de pied de page**, **survol**. Aperçu + bouton **« réinitialiser »**.
- **Stockage** : clés `theme_color_*` dans `settings` (valeurs hex).
- **Rendu** : injection dans le `<head>` public sous forme de **variables CSS**
  (`:root { --color-bg: …; --color-text: …; --color-accent: … }`), surchargeant au besoin les
  variables Bootstrap 5 (`--bs-*`). Le CSS du site **référence les variables** (aucune valeur en dur).
- **Défauts** (proches de la production actuelle) : fond `#e6e6e6`, texte `#7a7a7a`, accent `#e2041a`.
- **Sécurité** : valider que chaque valeur est une couleur hexadécimale (anti-injection CSS).

> Choix de framework permettant ce réglage simple : **Bootstrap 5** et ses variables CSS (décision D-11).

## 12. Menu de navigation responsive (hamburger) — *exigence client*

Comportement calqué sur https://www.brunstatt-didenheim.fr/ **pour le fonctionnement uniquement**
(les couleurs viennent du thème, §11). L'ancien site n'avait **aucun menu** (uniquement des tuiles).

- **En-tête** : logo à gauche + zone de navigation à droite.
- **Grand écran** (≥ point de rupture, proposé `lg`/992px) : **menu horizontal** déployé.
- **Mobile** (< point de rupture) : **bouton hamburger** qui ouvre un **panneau off-canvas**
  (glisse depuis un bord, par-dessus la page, avec voile sombre — **choix client**, géré nativement par BS5).
- **Contenu** : **Accueil** + **galeries publiées** (triées par `sort_order`) + **Contact** (si activé).
  Sous-menus seulement si l'imbrication de galeries est un jour activée (H-01).
- **Implémentation** : `navbar` Bootstrap 5 + composant **off-canvas** (`navbar-toggler` + `offcanvas`), **sans jQuery**.
- **Accessibilité** : `aria-expanded`, `aria-controls`, focus clavier, cible tactile suffisante.
- **Réglages** (`settings`) : `menu_breakpoint`, `menu_mode`, `menu_show_contact`.

## 13. Chargement progressif des images (lazy-load) — *exigence client*

Interprétation du « upload on load » : **chargement différé des images au défilement** dans les
galeries (et listes d'images), **paramétrable** depuis l'admin.

- **Réglages admin (confirmés par le client)** : **utiliser ou non** le lazy-load
  (`gallery_lazyload_enabled`, on/off) et le **nombre d'images par lot** (`gallery_batch_size`).
- **Comportement** : afficher un **premier lot** puis charger les lots suivants (même taille) au
  défilement, en **JavaScript moderne** (IntersectionObserver, sans jQuery) — reprend la logique de
  chargement progressif de l'ancien `index.php`. Si **off**, toutes les images sont rendues d'emblée.
- **Complément** : attribut `loading="lazy"` natif sur les `<img>` + dimensions connues (anti-CLS).
- **Bénéfice** : performance sur les galeries fournies (ex. *Bouches à clef* = 22 images).

## 14. Page « Mentions légales » — *exigence client*

- **Modèle** : une entrée de la table `pages` (slug `mentions-legales`).
- **Édition** : l'admin gère la page via **un seul champ HTML éditable** (`body_html`), avec l'éditeur
  riche **TinyMCE** (D-18). Titre par défaut « Mentions légales ».
- **Activation** : **activable/désactivable** depuis l'admin via le **statut** de la page
  (`published` = visible, `draft` = masquée). Pas de réglage séparé.
- **Rendu public** : un **lien en pied de page** apparaît uniquement si la page est publiée ; l'URL
  `/mentions-legales` renvoie une **404** si elle est masquée.
- **SEO** : `noindex` recommandé par défaut (page légale), modifiable.

> Voir `architecture.md` (composants, arborescence), `migration.md` (bascule, risques),
> `decisions.md` (choix techniques, hypothèses, questions ouvertes).
