# Rapport — Correction JSON-LD et carrousel de la page d'accueil

Date de rédaction : 22 juillet 2026.

## 1. Cause exacte du JSON-LD visible en haut de page

**Cause racine** : `resources/views/components/layout.blade.php` affichait la variable
`$structuredData` avec la syntaxe Blade `{{ $structuredData ?? '' }}` — cette syntaxe appelle
automatiquement `htmlspecialchars()` sur son contenu. Or `$structuredData`, construite dans
`home.blade.php`, était déjà une **chaîne HTML complète** (`'<script type="application/ld+json">'
.json_encode(...).'</script>'`). L'échappement automatique transformait donc `<script>` en
`&lt;script&gt;`, ce qui affichait le tag comme du **texte visible** en haut de la page au lieu
d'être interprété comme une vraie balise `<script>` par le navigateur.

Un second problème existait en parallèle : `json_encode()` n'utilisait que
`JSON_UNESCAPED_SLASHES`, sans `JSON_UNESCAPED_UNICODE` — les caractères accentués français
(« Personnalité », « République », « démocratique ») auraient été rendus sous forme d'séquences
`\uXXXX` échappées.

### Correctif appliqué

- [resources/views/home.blade.php](../resources/views/home.blade.php) : `$structuredData` est
  désormais un **tableau PHP simple** (les données brutes), plus une chaîne HTML pré-construite.
- [resources/views/components/layout.blade.php](../resources/views/components/layout.blade.php) :
  le tableau est transformé en JSON-LD **dans le layout**, avec une vraie balise `<script>` écrite
  directement en Blade (jamais échappée), et le JSON généré via
  `json_encode($structuredData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)`. Une neutralisation
  défensive de toute sous-chaîne `</script` (remplacée par `<\/script`, qui reste du JSON valide)
  empêche par ailleurs toute fermeture prématurée de la balise si une valeur venait un jour à
  contenir ce texte.

```blade
@if (!empty($structuredData))
    @php
        $jsonLd = str_replace('</script', '<\/script', json_encode($structuredData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    @endphp
    <script type="application/ld+json">{!! $jsonLd !!}</script>
@endif
```

### Validation

- Le JSON extrait du HTML servi par `http://tlt.test/` parse sans erreur avec `json_decode()`.
- Aucune séquence `é` n'apparaît dans la sortie ; les caractères accentués sont littéraux.
- Le début du `<body>` (avant le `<header>`) ne contient plus aucune trace de
  `application/ld+json` ou `@context` — testé automatiquement (voir §6).

## 2. Modèle de données du carrousel

Nouveaux champs sur `media_items` (migration
`add_hero_carousel_fields_to_media_items_table`) :

| Champ | Rôle |
|---|---|
| `hero_candidate` (déjà existant) | Suggestion technique automatique (générée par `media:scan`) |
| `hero_active` | Sélection éditoriale réelle — l'image apparaît dans le carrousel du site |
| `hero_position` | Ordre d'affichage dans le carrousel |
| `hero_title` | Titre optionnel de la diapositive |
| `hero_caption` | Légende discrète optionnelle affichée en bas de l'image |
| `hero_focal_x` / `hero_focal_y` | Point focal (0-100), converti en `object-position` CSS |

Limite configurable : `config('media.hero_limit')` (variable d'environnement
`MEDIA_HERO_LIMIT`, par défaut **6**).

## 3. Sélection initiale des images

Quatre images ont été choisies à la main dans la médiathèque existante (voir
`database/seeders/HeroSeeder.php`), pour raconter le parcours dans l'ordre demandé :

| Ordre | Image | Titre | Légende | Justification |
|---|---|---|---|---|
| 1 | `img-05109` (`TLT_2025_web.jpg`) | Une voix pour Kinshasa | Responsabilités institutionnelles | Portrait institutionnel récent (2025), haute résolution, drapeau RDC, déjà utilisé comme portrait de référence |
| 2 | `img-00229` (`Le Bourgmestre au travail/20160419_110225...`) | Sur le terrain | Engagement sur le terrain | Action de terrain authentique (coordination avec la police lors de travaux), visage clairement identifiable, format paysage natif |
| 3 | `img-05161` (`tenge_images/WhatsApp...15.29.34.jpeg`) | Proche des habitants | Au contact de la population | Proximité directe avec une habitante, écharpe « Député Provincial Kintambo N°4 », déjà retouchée dans la médiathèque |
| 4 | `img-05144` (`tenge_images/WhatsApp...15.29.30.jpeg`) | Porter une vision | Une vision pour la ville | Intervention médiatique (« Le Débat », Top Congo FM), cadrage frontal net, activité publique/officielle |

Critères appliqués : personne clairement identifiable et non floue, diversité des contextes
(portrait / terrain / proximité / média), bonne résolution, cadrage exploitable en paysage,
absence de doublons entre les quatre choix. Un administrateur peut changer cette sélection à tout
moment depuis `/admin/media/hero` — cette sélection initiale n'est qu'un point de départ.

## 4. Interface d'administration — `/admin/media/hero`

Page dédiée « Carrousel de la page d'accueil », accessible depuis le menu admin (nouvel élément
entre « Ajouter des photos » et « Albums »). Fonctionnalités :

- Liste des images actives, dans l'ordre du carrousel réel du site.
- Réorganisation par glisser-déposer (JavaScript natif, sauvegarde immédiate via `fetch`).
- Modification du titre, de la légende et du **point focal** par simple clic sur l'aperçu (un
  marqueur visuel indique le point focal actuel, converti en `object-position` sur le site public).
- Retrait du carrousel sans suppression de l'image (reste dans la médiathèque).
- Ajout de nouvelles images depuis une grille de candidates (images publiées, non déjà actives,
  triées en priorité par `hero_candidate`/`featured`/résolution), avec recherche.
- Limite configurable affichée en permanence (« X / 6 images actives ») ; tentative d'ajout au-delà
  de la limite bloquée avec message clair.
- Lien direct « Voir la page d'accueil ↗ » pour prévisualiser le rendu réel après chaque
  changement (les actions s'appliquent immédiatement, pas de brouillon séparé).

### Routes ajoutées

```
GET    /admin/media/hero                admin.media.hero
POST   /admin/media/hero/reorder        admin.media.hero.reorder
POST   /admin/media/hero/{media}/add    admin.media.hero.add
DELETE /admin/media/hero/{media}        admin.media.hero.remove
PUT    /admin/media/hero/{media}        admin.media.hero.update
```

## 5. Carrousel public (page d'accueil)

Remplace l'unique image de fond du hero par une composition **texte à gauche / carrousel à
droite** (conservée en une seule colonne empilée sur mobile). Implémentation en JavaScript léger
natif (aucune librairie ajoutée), dans `resources/js/app.js` :

- Défilement automatique toutes les 6,5 secondes, en pause au survol de la souris **et** au focus
  clavier.
- Flèches précédent/suivant et indicateurs de position (`role="tablist"`), tous avec libellés
  accessibles (`aria-label`).
- Navigation clavier (flèches gauche/droite) lorsque le carrousel a le focus (`tabindex="0"`,
  `role="region"`, `aria-roledescription="carrousel"`).
- Gestes tactiles (balayage) sur mobile.
- Respect de `prefers-reduced-motion` : le défilement automatique est désactivé, les contrôles
  manuels restent utilisables.
- **Fallback sans JavaScript** : chaque diapositive est positionnée en absolu avec opacité
  0/100 gérée en CSS ; sans script, la première image reste visible normalement (aucun script
  requis pour afficher un hero fonctionnel), sans saut de mise en page.
- Première image en `loading="eager"` + `fetchpriority="high"`, les suivantes en
  `loading="lazy"` — cohérent avec la demande de chargement rapide et priorisé.
- Chaque image expose `width`/`height` réels (évite tout saut de mise en page/CLS) et un texte
  alternatif basé sur `hero_title` ou, à défaut, le texte alternatif existant de la photo.

## 6. Protection et robustesse

- **Dépendance suivie** : `MediaItem::usageSummary()` inclut désormais `Carrousel accueil`.
  Une image encore active dans le carrousel **ne peut pas être supprimée définitivement** sans
  cocher « Retirer les associations », qui vide alors proprement `hero_active`/`hero_position`
  avant suppression (testé, voir §7).
- **Dépublication** : reste possible, mais un message de confirmation JavaScript avertit
  explicitement l'administrateur que l'image sortira du carrousel affiché aux visiteurs tant
  qu'elle n'est pas republiée.
- **Corbeille** : le retrait logique (soft delete) exclut automatiquement l'image de
  `MediaItem::scopeHeroActive()` (la portée `published()` et le scope global des éléments
  supprimés l'excluent tous les deux) — la page d'accueil ne casse jamais.
- **Image manquante** : `HomeController` filtre les images actives dont la variante `large` n'existe
  plus sur le disque avant de les transmettre à la vue ; si aucune image active n'est finalement
  disponible, une image de secours est utilisée automatiquement (portrait par défaut ou première
  candidate hero disponible).
- **Badges dans la médiathèque** : un badge distinct « Dans le carrousel » (grille et vue tableau)
  différencie clairement une image *active* d'une simple *candidate hero* (« Candidate hero »),
  conformément à la demande de ne pas se contenter d'une case à cocher unique.

## 7. Tests exécutés et résultats

```
php artisan test
56 tests, 154 assertions — 56 passed, 0 failed
```

14 nouveaux tests dédiés ([tests/Feature/HeroCarouselTest.php](../tests/Feature/HeroCarouselTest.php)) :
JSON-LD non affiché comme texte visible, JSON-LD présent et valide, absence de séquences unicode
échappées, la page d'accueil affiche au moins 4 images actives, seules les images publiées sont
utilisées, l'ordre du carrousel respecte `hero_position`, une image de secours est utilisée en
l'absence de sélection, une variante manquante est ignorée sans casser la page, ajout/retrait
depuis l'admin, respect de la limite configurée, changement de position (réorganisation),
modification du titre/légende/point focal, permissions (accès admin requis), protection contre la
suppression définitive d'une image encore active dans le carrousel (avec levée contrôlée après
retrait des associations).

Les tests écrivent dans un dossier d'archives temporaire isolé (comme pour la médiathèque) : **le
dossier `images/` réel n'a pas été touché** (vérifié après coup : toujours 5 333 fichiers).

### Vérification manuelle sur `http://tlt.test`

- Le JSON `<script type="application/ld+json">` est bien placé dans `<head>`, jamais affiché comme
  texte au-dessus du header (vérifié par extraction et inspection du HTML servi).
- Le carrousel affiche ses 4 diapositives (`hero-slide` × 4), 4 indicateurs de position
  (« Aller à la photo 1 sur 4 » … « 4 sur 4 »), boutons précédent/suivant avec libellés
  accessibles, et les 4 légendes attendues (« Responsabilités institutionnelles »,
  « Engagement sur le terrain », « Au contact de la population », « Une vision pour la ville »).
- Aucune classe CSS à largeur fixe non responsive détectée dans le template du hero.
- **Limite de l'environnement** : le panneau navigateur intégré à cet environnement de
  développement bloque les outils de capture d'écran/lecture pour le domaine personnalisé
  `tlt.test` derrière une carte d'approbation manuelle que je ne peux pas valider moi-même
  (limitation déjà rencontrée et documentée lors du diagnostic précédent). La vérification
  visuelle humaine directe dans un navigateur (netteté des images, cadrage du visage, absence de
  débordement mobile) reste donc recommandée en complément de cette validation automatisée et
  textuelle.

## 8. Procédure — changer les images du carrousel

1. Se connecter à `/admin`.
2. Aller dans « Carrousel d'accueil » (`/admin/media/hero`).
3. Retirer une image existante si nécessaire (bouton « Retirer », ne la supprime pas).
4. Ajouter une nouvelle image depuis la grille de candidates, ou depuis la médiathèque en
   utilisant le filtre « Candidate hero »/« Dans le carrousel ».
5. Réorganiser par glisser-déposer, ajuster titre/légende, cliquer sur l'aperçu pour définir le
   point focal.
6. Cliquer sur « Voir la page d'accueil ↗ » pour valider le rendu réel — les changements sont
   déjà appliqués (pas d'étape de publication séparée).

## 9. Fichiers créés ou modifiés

- Migration `2026_07_22_081546_add_hero_carousel_fields_to_media_items_table.php`
- `config/media.php` (ajout `hero_limit`)
- `app/Models/MediaItem.php` (champs, `scopeHeroActive`, `largeUrlAbsolute()`, `focalPointCss()`,
  `usageSummary()` mis à jour)
- `app/Http/Controllers/Admin/HeroAdminController.php` (nouveau)
- `app/Http/Controllers/HomeController.php` (sélection des images actives + filet de sécurité)
- `app/Http/Controllers/Admin/MediaTrashController.php` (détachement du carrousel lors d'une
  suppression définitive confirmée)
- `resources/views/home.blade.php` (JSON-LD corrigé, hero transformé en carrousel)
- `resources/views/components/layout.blade.php` (rendu JSON-LD sécurisé)
- `resources/views/admin/media/hero.blade.php` (nouveau)
- `resources/views/admin/media/index.blade.php` et `edit.blade.php` (badges, avertissements)
- `resources/views/components/admin-layout.blade.php` (lien de menu, messages de statut)
- `resources/js/app.js` (logique du carrousel)
- `database/seeders/HeroSeeder.php` (sélection initiale, reproductible)
- `tests/Feature/HeroCarouselTest.php` (nouveau)

Rien n'a été retiré à la médiathèque existante (upload, corbeille, albums, journal d'audit,
protection des dépendances) : ce travail vient s'y ajouter.
