Flow actuel du feed Acteur
Statut : Audit de l'état provisoire — lecture seule, aucune modification du code applicatif
Date : 2026-07-06
Portée : dmv-public — page fiche acteur
Architecture cible : 34-ranking-engine-synthesis.md — ce document décrit l'état en place ; le doc 34 définit vers quoi migrer
1. Résumé
La page acteur est un hybride SSG + client. Next.js génère statiquement les fiches à la construction (generateStaticParams) en chargeant toutes les données de l'acteur depuis Supabase côté serveur. Le composant client ActorsClient prend le relais pour les interactions (tabs, modaux, statut d'ouverture en temps réel). Le feed de publications est rendu par PublicationsFeed, un composant client autonome qui gère lui-même son fetch Supabase, son cache localStorage (fenêtre 3 jours) et sa pagination infinie via IntersectionObserver. L'ordre est strictement chronologique décroissant (publie_le DESC), sans scoring ni personnalisation.
2. Schéma du flux
app/[commune]/acteur/[slug]/page.tsx ← SSG, charge acteur + claims
└── ActorsClient.tsx ← Shell client, tabs, interactions
├── [Tab "Mur"]
│ └── PublicationsFeed.tsx ← fetch Supabase + cache localStorage
│ └── PublicationCard (×N)
│
├── [Tab "Infos pratiques"] ← horaires détaillés, documents
│
└── [Tab "Commune"] ← chargé à la demande (mairie uniquement)
3. Fichiers exacts
dmv-public/app/[commune]/acteur/[slug]/page.tsx
- Rôle : Page serveur Next.js, point d'entrée SSG de la fiche acteur
- Ce qu'il décide :
generateStaticParams(): génère toutes les combinaisons(commune, slug)depuis Supabase- Chargement serveur de toutes les données acteur (id, slug, nom, kind, description, coordonnées, horaires, badges, categories, tags, has_alert)
- Lookup table
claimspouris_claimed - Génération métadonnées SEO et JSON-LD (Organisation, LocalBusiness)
notFound()si acteur absent ou invalide
- Ce qu'il délègue : Tout le rendu et l'interactivité à
ActorsClient - Logique métier : Faible — orchestration de fetch côté serveur, pas de calcul
dmv-public/app/[commune]/acteur/[slug]/ActorsClient.tsx
- Rôle : Shell client principal de la fiche acteur (~1 400 lignes)
- Ce qu'il décide :
- Gestion de l'état tab actif (mur / infos / commune)
- Chargement différé des données commune (uniquement quand l'onglet "Commune" est ouvert)
- Statut d'ouverture en temps réel via
getDetailedOpenStatus(horaires)(parse JSON horaires, compare heure actuelle, calcule prochain créneau) - Affichage conditionnel : mode
onlyEpingleou feed complet - Gestion des modaux : contact, revendication, carte
- Tracking des interactions utilisateur
- Ce qu'il délègue :
- Feed de publications →
PublicationsFeed - Rendu carte individuelle →
PublicationCard
- Feed de publications →
- Logique métier : Oui — statut d'ouverture, composition des sections, gestion des cas propriétaire/non-propriétaire
dmv-public/components/publications/PublicationsFeed.tsx
- Rôle : Composant de feed publications d'un acteur avec cache et pagination infinie (~279 lignes)
- Ce qu'il décide :
- Stratégie de cache : lit
dmv_actor_publications_{acteurId}_{all|pinned}(localStorage, fenêtre 3 jours) - Stratégie de fetch :
- Affiche le cache immédiatement (optimistic display)
- Fetch Supabase :
publie_le >= cacheCutoff AND <= now,ORDER BY publie_le DESC,LIMIT 100 - Si résultat vide → fallback fetch
publie_le < cacheCutoff,LIMIT pageSize - Merge cache + incoming, déduplication par
id, re-tri par date
- Détection publications "fraîches" (
getFreshPublicationIds) → badge "Nouveauté" - Pagination infinie :
IntersectionObserversur sentinel,rootMargin: "500px 0px", curseur =publie_ledu dernier item batchSizeconfigurable via/api/v1/settings/publications_scroll_batch_size(défaut 24, bornes [6..60])- Filtre optionnel
epingle = truesionlyEpingleprop
- Stratégie de cache : lit
- Ce qu'il délègue : Rendu de chaque carte →
PublicationCard - Logique métier : Oui — stratégie de cache, fenêtre temporelle, pagination, détection fraîcheur
dmv-public/components/publications/PublicationCard.tsx
- Rôle : Carte d'une publication individuelle
- Ce qu'il décide : Affichage badge "Nouveauté" (
isNew), variantcondensed(en-tête seul) ouexpanded(publication complète) - Ce qu'il délègue : Rien
- Logique métier : Non — rendu pur
dmv-public/lib/publicationCache.ts
- Rôle : Gestion du cache localStorage des publications
- Ce qu'il décide :
PUBLICATION_CACHE_WINDOW_MS= 3 × 24 × 60 × 60 × 1000 (3 jours)mergePublications(current, incoming): merge parid, tri parpublie_ledécroissantprunePublicationCache(items): supprime les items hors fenêtre de cachegetFreshPublicationIds(cached, incoming): IDs présents dansincomingmais absents decachedreadPublicationCache: lit, parse, prune automatiquement, réécrit si pruningwritePublicationCache: prune avant écriture
- Ce qu'il délègue : Rien
- Logique métier : Oui — politique de cache, déduplication, fraîcheur
dmv-public/lib/publicationSettings.ts
- Rôle : Configuration du batch size de pagination
- Ce qu'il décide : Défaut = 24, range [6..60], fetch depuis
/api/v1/settings/publications_scroll_batch_size - Ce qu'il délègue : Rien
- Logique métier : Non — configuration
4. Flux de chargement
[Build time - SSG]
page.tsx
→ generateStaticParams() → toutes les (commune, slug)
→ fetch Supabase : acteur complet + claims
→ génère métadonnées JSON-LD
[Runtime - Rendu initial]
page.tsx → ActorsClient (props : acteur + isClaimed)
ActorsClient :
- affiche hero, coordonnées, onglet "Mur" actif par défaut
- calcule getDetailedOpenStatus(horaires) → isOpen, label, prochaine ouverture
[Après mount - PublicationsFeed]
1. readPublicationCache(cacheKey) → affiche immédiatement si cache présent
2. fetchPublicationScrollBatchSize() → met à jour le pageSize
3. Supabase query :
SELECT id, titre, contenu, image_url, image_path, publie_le,
date_debut, lieu, epingle, type_publications(nom, couleur)
FROM publications
WHERE acteur_id = filter.acteurId
AND valide = true
AND deleted_at IS NULL
AND publie_le IS NOT NULL
AND publie_le >= cacheCutoff (now - 3 jours)
AND publie_le <= now
[AND epingle = true ← si onlyEpingle]
ORDER BY publie_le DESC
LIMIT 100
4. merge(cache, result) → déduplication + tri
5. writePublicationCache(cacheKey, merged)
6. Si merged vide → fallback query : publie_le < cacheCutoff, LIMIT pageSize
7. Affiche la liste, marque freshPubIds avec badge "Nouveauté"
[Scroll - Pagination]
IntersectionObserver sur <div ref={sentinelRef}> (rootMargin: 500px)
→ cursor = pubs[last].publie_le
→ Supabase : WHERE publie_le < cursor ORDER BY publie_le DESC LIMIT pageSize
→ merge avec liste existante
→ hasMore = (résultat.length === pageSize)
5. Tri et ordre
Tri unique : publie_le DESC (le plus récent en premier), appliqué à trois niveaux :
- Supabase query :
ORDER BY publie_le DESC mergePublications(): re-tri systématique parpublie_leaprès chaque merge- Pagination : curseur
lt("publie_le", cursor)garantit la continuité
Il n'existe aucun ranking, aucun score, aucune personnalisation dans le feed acteur. L'ordre est 100% chronologique.
Exception : publications épinglées
Quand onlyEpingle = true, filtre epingle = true ajouté à la query Supabase. Ces publications restent triées par publie_le DESC même épinglées — il n'y a pas d'ordre d'épinglage distinct.
6. Hooks et dépendances impliqués
| Élément | Fichier | Rôle |
|---|---|---|
readPublicationCache | lib/publicationCache.ts | Lecture + prune cache localStorage |
writePublicationCache | lib/publicationCache.ts | Écriture cache (toujours prunée) |
mergePublications | lib/publicationCache.ts | Fusion + déduplication + tri |
getFreshPublicationIds | lib/publicationCache.ts | Détection nouveautés |
isPublicationWithinCacheWindow | lib/publicationCache.ts | Test fenêtre 3 jours |
fetchPublicationScrollBatchSize | lib/publicationSettings.ts | Config batch size |
getDefaultPublicationScrollBatchSize | lib/publicationSettings.ts | Défaut = 24 |
pubImageUrl | lib/pubImageUrl.ts | Résolution URL image (storage Supabase) |
getDetailedOpenStatus | ActorsClient.tsx (inline) | Statut ouverture temps réel |
IntersectionObserver | PublicationsFeed.tsx (useEffect) | Déclencheur pagination |
7. Logique métier dispersée
Tri / ordre
Où : publicationCache.ts → mergePublications() + query Supabase dans PublicationsFeed.tsx
Entièrement chronologique décroissant. Aucune logique de priorisation.
Cache
Où : publicationCache.ts + PublicationsFeed.tsx
Fenêtre 3 jours (PUBLICATION_CACHE_WINDOW_MS). Prune automatique à chaque lecture. Clé : dmv_actor_publications_{acteurId}_{all|pinned}.
Déduplication
Où : publicationCache.ts → mergePublications()
Par id (Map), puis re-tri date. Garantit qu'une même publication n'apparaît pas deux fois après merge cache/fetch.
Fraîcheur / nouveautés
Où : publicationCache.ts → getFreshPublicationIds() + PublicationsFeed.tsx
IDs présents dans incoming mais absents de cached → badge "Nouveauté" (bleu) dans PublicationCard.
Pagination
Où : PublicationsFeed.tsx + publicationSettings.ts
IntersectionObserver (rootMargin 500px), curseur publie_le, batch configurable [6..60], défaut 24.
Statut d'ouverture
Où : ActorsClient.tsx → getDetailedOpenStatus(horaires)
Parse JSON des horaires, compare heure courante, retourne { isOpen, label, nextOpenLabel }. Logique inline dans le composant — non extraite.
Publications épinglées
Où : PublicationsFeed.tsx prop onlyEpingle
Filtre Supabase epingle = true. Cache séparé (_pinned). Même ordre chronologique.
8. Cas particulier : Mon Espace acteur vs fiche acteur
Il existe deux chemins distincts pour afficher les publications d'un acteur :
| Chemin | Fichier | Via WallEngine ? | Cache | Pagination |
|---|---|---|---|---|
| Fiche acteur | components/publications/PublicationsFeed.tsx | Non | localStorage 3 jours | Infinie (IntersectionObserver) |
| Mon Espace → mode acteur | app/components/mon-espace/MonEspaceActorFeed.tsx | Non | localStorage (dmv_actor_pubs_v1) | Statique (LIMIT 20) |
Les deux bypasses WallEngine. La fiche acteur est plus complète (fenêtre cache plus longue, pagination réelle).
9. Conclusion
Meilleur point d'entrée pour migration ou évolution
PublicationsFeed.tsx — c'est lui qui gère l'intégralité du cycle de données (fetch, cache, pagination, fraîcheur). Toute évolution du tri ou de la personnalisation passerait par ce fichier.
Fichiers à ne pas toucher au début
publicationCache.ts— stratégie de cache sensible, utilisée par les deux chemins (fiche + Mon Espace acteur viaMonEspaceActorFeed)PublicationCard.tsx— composant feuille partagé avec WallEngine
Candidats à extraction
getDetailedOpenStatusdansActorsClient.tsx— logique métier inline (statut ouverture), candidat à déplacement danslib/horairesUtils.ts- La double voie fiche acteur / Mon Espace acteur — logique dupliquée, candidat à unification sur
PublicationsFeedavec un propacteurId