Aller au contenu principal

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 claims pour is_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 onlyEpingle ou 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
  • 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 :
      1. Affiche le cache immédiatement (optimistic display)
      2. Fetch Supabase : publie_le >= cacheCutoff AND <= now, ORDER BY publie_le DESC, LIMIT 100
      3. Si résultat vide → fallback fetch publie_le < cacheCutoff, LIMIT pageSize
      4. Merge cache + incoming, déduplication par id, re-tri par date
    • Détection publications "fraîches" (getFreshPublicationIds) → badge "Nouveauté"
    • Pagination infinie : IntersectionObserver sur sentinel, rootMargin: "500px 0px", curseur = publie_le du dernier item
    • batchSize configurable via /api/v1/settings/publications_scroll_batch_size (défaut 24, bornes [6..60])
    • Filtre optionnel epingle = true si onlyEpingle prop
  • 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), variant condensed (en-tête seul) ou expanded (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 par id, tri par publie_le décroissant
    • prunePublicationCache(items) : supprime les items hors fenêtre de cache
    • getFreshPublicationIds(cached, incoming) : IDs présents dans incoming mais absents de cached
    • readPublicationCache : lit, parse, prune automatiquement, réécrit si pruning
    • writePublicationCache : 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 :

  1. Supabase query : ORDER BY publie_le DESC
  2. mergePublications() : re-tri systématique par publie_le après chaque merge
  3. 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émentFichierRôle
readPublicationCachelib/publicationCache.tsLecture + prune cache localStorage
writePublicationCachelib/publicationCache.tsÉcriture cache (toujours prunée)
mergePublicationslib/publicationCache.tsFusion + déduplication + tri
getFreshPublicationIdslib/publicationCache.tsDétection nouveautés
isPublicationWithinCacheWindowlib/publicationCache.tsTest fenêtre 3 jours
fetchPublicationScrollBatchSizelib/publicationSettings.tsConfig batch size
getDefaultPublicationScrollBatchSizelib/publicationSettings.tsDéfaut = 24
pubImageUrllib/pubImageUrl.tsRésolution URL image (storage Supabase)
getDetailedOpenStatusActorsClient.tsx (inline)Statut ouverture temps réel
IntersectionObserverPublicationsFeed.tsx (useEffect)Déclencheur pagination

7. Logique métier dispersée

Tri / ordre

: publicationCache.tsmergePublications() + query Supabase dans PublicationsFeed.tsx
Entièrement chronologique décroissant. Aucune logique de priorisation.

Cache

: 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

: publicationCache.tsmergePublications()
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

: publicationCache.tsgetFreshPublicationIds() + PublicationsFeed.tsx
IDs présents dans incoming mais absents de cached → badge "Nouveauté" (bleu) dans PublicationCard.

Pagination

: PublicationsFeed.tsx + publicationSettings.ts
IntersectionObserver (rootMargin 500px), curseur publie_le, batch configurable [6..60], défaut 24.

Statut d'ouverture

: ActorsClient.tsxgetDetailedOpenStatus(horaires)
Parse JSON des horaires, compare heure courante, retourne { isOpen, label, nextOpenLabel }. Logique inline dans le composant — non extraite.

Publications épinglées

: 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 :

CheminFichierVia WallEngine ?CachePagination
Fiche acteurcomponents/publications/PublicationsFeed.tsxNonlocalStorage 3 joursInfinie (IntersectionObserver)
Mon Espace → mode acteurapp/components/mon-espace/MonEspaceActorFeed.tsxNonlocalStorage (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 via MonEspaceActorFeed)
  • PublicationCard.tsx — composant feuille partagé avec WallEngine

Candidats à extraction

  • getDetailedOpenStatus dans ActorsClient.tsx — logique métier inline (statut ouverture), candidat à déplacement dans lib/horairesUtils.ts
  • La double voie fiche acteur / Mon Espace acteur — logique dupliquée, candidat à unification sur PublicationsFeed avec un prop acteurId