# Nouveau template de page : Hub Actualités (grille + filtre par tag)

## 1. Contexte

Le template de détail "actualité" existe déjà (conception + implémentation technique complètes : entité `Page` avec `template = 'actualite'`, `ActualitePageEnricher`, `ActualitePageContentModel`, tags via l'entité `Tag` en relation ManyToMany sur `Page`). Il manque aujourd'hui une page "hub" qui liste l'ensemble des actualités publiées sous forme de grille, avec un filtre par tag, pour donner un point d'entrée unique vers le contenu éditorial du site.

Ce nouveau template ("actualites_hub" ou nom équivalent) suit le pattern DTO / Enricher / Service déjà en place dans le projet (voir `CLAUDE.md` section "When adding a new template type" et les services `src/Page/Actualite/*` comme référence d'architecture, bien qu'il s'agisse ici d'un template différent avec ses propres DTO/Enricher/Service dans `src/Page/ActualiteHub/` ou équivalent).

Contrainte produit forte : la page hub ne doit pas re-solliciter le serveur à chaque changement de filtre — le filtrage par tag est **100% client-side** une fois la grille chargée, pour un rendu instantané. Autre contrainte forte : ne charger, pour cette page, que les données strictement nécessaires à l'affichage d'une carte (titre, image principale, tag(s), date de dernière modification) — **jamais** le contenu complet (`content.blocks`) d'un article, pour éviter un payload lourd et des enrichissements en cascade inutiles (pas de `resolveBlockImages`, pas de `resolveNeighbors`, etc. pour la liste).

Référence Figma : https://www.figma.com/design/ldjPgBvJ6j2I9RVTa17H33/Galta_2026?node-id=143-1125&t=rDRwxupjegQSSHpz-4

**Note d'implémentation** : l'accès Figma MCP (`get_design_context`) a atteint sa limite d'appels au moment de la rédaction de cette story (siège "View" du plan Professional). Les specs exactes de layout de carte, espacements, typographie et UI de filtre n'ont pas pu être extraites automatiquement. Le développeur qui prend cette story doit ouvrir le lien Figma ci-dessus (ou relancer `get_design_context` / `get_screenshot` une fois le quota disponible) et compléter les mesures exactes (grid-gap, dimensions de carte, ratio image, style des boutons de filtre) avant/pendant l'implémentation SCSS. Les critères d'acceptation ci-dessous couvrent le comportement fonctionnel (grille, filtre, tri, responsive, payload) qui est, lui, non ambigu.

## 2. Le besoin

```
En tant que visiteur du site
Je souhaite consulter toutes les actualités publiées dans une grille et pouvoir filtrer instantanément par tag
Afin de retrouver rapidement les articles qui m'intéressent sans attendre de rechargement de page
```

## 3. Liens utiles

- **Figma (node hub actualités)** : https://www.figma.com/design/ldjPgBvJ6j2I9RVTa17H33/Galta_2026?node-id=143-1125&t=rDRwxupjegQSSHpz-4 — layout de grille (carte + filtre) à extraire précisément via Figma MCP quand le quota sera disponible ; à date, seules la structure fonctionnelle et les breakpoints (3/2/1 colonnes) sont confirmés par le PO.
- **Template de détail actualité (référence d'architecture)** : `src/Page/Actualite/Service/ActualitePageEnricher.php`, `src/Page/Actualite/Model/ActualitePageContentModel.php`, `templates/pages/actualite.html.twig`
- **TemplateMatcher** (à compléter avec la nouvelle entrée) : `src/Service/TemplateMatcher.php`
- **Entité Tag et relation ManyToMany Page↔Tag** : `src/Entity/Tag.php`, `src/Entity/Page.php` (`getTags()`, table `page_tag`)
- **PageRepository** (méthode `findPublishedByTemplate()` existante, réutilisable comme base pour une requête optimisée dédiée au hub) : `src/Repository/PageRepository.php`
- **TagRepository** (`findAllOrderedByName()`) : `src/Repository/TagRepository.php`
- **Existing "children grid" pattern à ne PAS copier tel quel** (grille simple sans filtre, template `list`) : `templates/pages/list.html.twig` — utile comme repère de structure Twig grid, mais ne gère ni tags ni filtre client-side.
- **Pattern DTO/Enricher/Service pour un nouveau template** : `CLAUDE.md` section "When adding a new template type (RECOMMENDED APPROACH)"
- **Endpoint tags existant (admin-only aujourd'hui)** : `src/Controller/Admin/Api/Tag/TagApiController.php` (`GET /api/admin/tags`) — à examiner si un point d'accès public est nécessaire pour alimenter le filtre, ou si les tags peuvent être injectés directement côté serveur (SSR) dans la page hub. À trancher en board technique — voir note d'ambiguïté ci-dessous.
- **Story tags/CRUD associée** : `stories/actualite-tag-entity-admin-crud.md`
- **Related story (blocks dynamiques du détail actualité, contexte)** : `stories/actualite-inline-editor-dynamic-blocks.md`

## 4. Critères d'acceptation

**Note d'ambiguïté (à confirmer avec le PO avant dev)** : le filtre par tag est-il **single-select** (un seul tag actif à la fois, style "onglet") ou **multi-select** (plusieurs tags cumulables, logique OU/ET à préciser) ? Les scénarios ci-dessous couvrent le comportement multi-tag en OU (un article s'affiche s'il possède AU MOINS un des tags actifs) car c'est le comportement le plus courant pour ce type de UI filtre-pills, mais ceci doit être validé avant l'implémentation. Un scénario "single-select" alternatif est fourni à titre de repli si le PO tranche autrement.

```gherkin
Scénario: Affichage de la grille avec toutes les actualités publiées
  Étant donné qu'il existe 7 pages publiées avec le template "actualite"
  Et qu'il existe 2 pages "actualite" non publiées
  Quand un visiteur accède à la page hub actualités
  Alors la grille affiche exactement 7 cartes
  Et aucune carte ne correspond à une actualité non publiée
  Et aucune pagination n'est présente sur la page

Scénario: Tri par date de dernière modification décroissante
  Étant donné 3 actualités publiées A, B, C
  Et A a été modifiée le 01/01/2026, B le 15/03/2026, C le 10/02/2026
  Quand la grille est affichée
  Alors les cartes apparaissent dans l'ordre B, C, A
  Et cet ordre est déterminé côté serveur (pas de tri client-side)

Scénario: Chargement de données optimisé par carte (pas de N+1, pas de contenu complet)
  Étant donné une actualité publiée possédant plusieurs blocs de contenu (texte, texte+image)
  Quand la page hub est enrichie côté serveur (ActualiteHubPageEnricher ou équivalent)
  Alors chaque carte expose uniquement : titre, chemin de l'image principale, tag(s) associés, date de dernière modification, slug/URL
  Et la structure "content.blocks" de l'actualité n'est ni chargée ni sérialisée dans le payload de la carte
  Et aucun appel additionnel (résolution d'images de blocs, calcul previous/next) n'est effectué pour la liste

Scénario: Rendu des filtres à partir des tags disponibles
  Étant donné que les actualités publiées portent collectivement les tags "Événement", "Presse" et "Partenariat"
  Quand la page hub est chargée
  Alors une pastille de filtre est rendue pour chacun des 3 tags utilisés par au moins une actualité publiée
  Et aucune pastille n'est rendue pour un tag qui n'est utilisé par aucune actualité publiée
  Et une option "Tous" (ou équivalent) est disponible pour réinitialiser le filtre

Scénario: Filtrage client-side multi-tag (comportement OU) sans rechargement
  Étant donné que la grille est chargée avec 7 cartes et que le filtre "Presse" est disponible
  Quand le visiteur clique sur la pastille "Presse"
  Alors seules les cartes dont les tags incluent "Presse" restent visibles, instantanément
  Et aucune requête réseau (fetch/XHR) n'est déclenchée lors du clic
  Quand le visiteur clique ensuite sur la pastille "Partenariat" (en plus de "Presse")
  Alors les cartes affichées sont celles ayant "Presse" OU "Partenariat"
  Quand le visiteur clique sur "Tous"
  Alors les 7 cartes redeviennent visibles

Scénario alternatif: Filtrage client-side single-tag (si le PO tranche pour un mode exclusif)
  Étant donné que la grille est chargée et que les filtres "Presse" et "Partenariat" sont disponibles
  Quand le visiteur clique sur la pastille "Presse"
  Alors seules les cartes taguées "Presse" sont visibles
  Quand le visiteur clique sur la pastille "Partenariat"
  Alors le filtre "Presse" est désactivé et seules les cartes taguées "Partenariat" sont visibles

Scénario: Grille vide après filtrage
  Étant donné que la grille est chargée
  Quand le visiteur active un filtre pour lequel aucune actualité visible ne correspond
  Alors un message "Aucune actualité ne correspond à ce filtre" (ou équivalent) est affiché à la place des cartes
  Et le message disparaît dès qu'un filtre compatible est réactivé

Scénario: Disposition responsive - desktop (3 colonnes)
  Étant donné une largeur de viewport ≥ 1024px (breakpoint desktop du projet)
  Quand la grille est affichée
  Alors les cartes sont réparties sur 3 colonnes

Scénario: Disposition responsive - tablette (2 colonnes)
  Étant donné une largeur de viewport comprise entre le breakpoint tablette et le breakpoint desktop
  Quand la grille est affichée
  Alors les cartes sont réparties sur 2 colonnes

Scénario: Disposition responsive - mobile (1 colonne)
  Étant donné une largeur de viewport < breakpoint tablette
  Quand la grille est affichée
  Alors les cartes sont réparties sur 1 colonne
  Et les pastilles de filtre restent accessibles et lisibles (wrap ou scroll horizontal selon Figma)

Scénario: Carte sans image principale
  Étant donné une actualité publiée sans image principale renseignée (content.mainImage vide)
  Quand la grille est affichée
  Alors la carte correspondante affiche un visuel de repli (placeholder) sans erreur de rendu

Scénario: Fallback bilingue sur les cartes
  Étant donné une actualité publiée dont le titre anglais (titleEn) est vide
  Quand un visiteur consulte la page hub en locale anglaise (/en/...)
  Alors la carte affiche le titre français en repli automatique (via TranslationHelper)
```

### Notes techniques complémentaires pour le développeur

- Créer une requête dédiée dans `PageRepository` (ex. `findPublishedByTemplateForHub(string $template)`) plutôt que de réutiliser `findPublishedByTemplate()` tel quel si celle-ci hydrate des associations non nécessaires (vérifier le plan de requête Doctrine / lazy-loading des tags pour éviter le N+1 sur `page.tags`, envisager un `leftJoin` + `addSelect` explicite sur `tags`).
- Suivre le pattern DTO/Enricher/Service : créer `src/Page/ActualiteHub/Service/ActualiteHubPageEnricher.php` (ou nom cohérent avec la convention `src/Page/Actualite/*`), en s'inspirant de `ActualitePageEnricher::enrich()` mais **sans** appeler `resolveBlockImages()` ni `resolveNeighbors()`.
- Enregistrer le nouveau template dans `TemplateMatcher::AVAILABLE_TEMPLATES` (ex. `'actualite_hub' => 'pages/actualite_hub.html.twig'`) et ajouter l'option dans `templates/admin/pages/create.html.twig`.
- Le filtrage étant 100% client-side, prévoir un fichier TypeScript dédié (respect de la règle "TypeScript Only", `spec-driven-development/rules/typescript-only.md`) qui manipule le DOM/attributs `data-tags` déjà rendus côté serveur — pas de second appel API pour filtrer.
- Vérifier si `GET /api/admin/tags` (actuellement sous `/admin/`, probablement protégé) doit être dupliqué en version publique, ou si les tags utilisés doivent simplement être calculés et injectés côté serveur dans le HTML de la page hub (recommandé, évite un appel réseau superflu et respecte la contrainte "pas de round-trip").
