Flux métier
Les workflows critiques pour LeSite : inscription + paiement, mode chantier et revalidation du contenu.
Inscription et paiement HelloAsso
Visiteur (LeSite)
│
├─ Clique "S'inscrire" sur atelier
│
└─→ Formulaire inscription (prénom, nom, email, phone opt, tier)
│
├─ Rewrite: POST /api/checkout
│ → LeHub: POST /api/checkout
│
└─→ LeHub (API) :
1. Valide email unique / capacité / tarif
2. Crée Registration (paymentStatus='pending')
3. Crée CheckoutIntent HelloAsso
4. Retour redirectUrl (valide 15 min)
│
└─ LeSite redirige vers HelloAsso
│
├─ Paiement HelloAsso
│
└─ HelloAsso webhook → LeHub
1. Valide token sécurité
2. Upsert Participant
3. UPDATE Registration (paymentStatus='paid')
4. Envoie l'email de confirmation
│
└─ HelloAsso redirige vers
`https://fresquesystemique.org/confirmation?orderId=...`
│
└─ Visiteur voit confirmation LeSite
Détails clés :
- LeSite n'a aucun rôle dans la validation ou la création de données : tout passe par le rewrite vers l'API LeHub.
- Le visiteur ne voit jamais la vraie URL du hub (rewrite transparent).
- L'e-mail de confirmation est émis par LeHub ; le reçu de paiement est émis par HelloAsso, aucune facture PDF n'est générée par le SI.
- LeSite ne reçoit pas de webhook de paiement : la page
/confirmationlit l'état réel en interrogeantGET /api/public/registration-status?checkoutIntentId=…du Hub, plutôt que de faire confiance aux paramètres d'URL.
Si HelloAsso n'est pas accessible :
- Endpoint
/api/checkoutretourne erreur 503. Visiteur voit un message. - LeHub gère la réconciliation quotidienne en cas de webhook perdu (voir documentation LeHub).
Mode chantier et pont d'authentification
Middleware LeSite (middleware.ts)
│
├─ À chaque requête (sauf assets)
│
├─ 1. Fetch GET /api/public/site-mode (cache 15 sec)
│ → LeHub retour {chantier: true/false}
│
├─ 2. SI chantier=false → laisse passer (PageNext)
│
└─ 3. SI chantier=true
│
├─ SI robot d'aperçu de lien → laisse passer (vraies balises OG)
│
├─ SI en-tête x-seo-crawler valide → laisse passer
│ (récupérateur de pages SEO du Hub)
│
├─ Transmet le cookie à GET /api/public/session du Hub
│ → SI isAdmin=true → laisse passer (admin voit le vrai site)
│
└─ SINON → rewrite vers /chantier
Détails clés :
- Flag
chantierpiloté depuis LeHub (/admin/settings→ Mode chantier), lu sur/api/public/site-mode. - Cookie de session partagé sur
.fresquesystemique.orgen production. - LeSite ne déchiffre pas le cookie : il le transmet au Hub, qui tranche. LeSite ne détient aucun secret d'authentification (voir Architecture).
- Le nom du cookie doit rester identique côté Hub (
lib/auth.ts) et côté LeSite (lib/hub-session.ts). - Robots d'aperçu de lien (LinkedIn, Facebook, Slack, WhatsApp, Telegram…) : laissés passer pour que le partage affiche les vraies balises OG.
Pièges :
- Un
AUTH_SESSION_COOKIE_NAMEdésynchronisé → le Hub ne voit aucun cookie → tout le monde atterrit sur/chantier, administrateurs compris. - Le flag est mis en cache 15 secondes : un changement dans le Hub prend jusqu'à 15 secondes à se propager, par nœud de rendu.
- Si l'appel au Hub échoue, le middleware laisse passer (
chantier = false) plutôt que de bloquer le site. robots.txtetsitemap.xmldoivent rester hors du matcher du middleware, sinon Google reçoit la page chantier en HTML.
Revalidation ISR et webhook
Admin LeHub enregistre un contenu (article, réglage SEO…)
│
└─→ Le Hub appelle
POST https://fresquesystemique.org/api/revalidate
Header : x-revalidate-secret: <valeur>
Corps : { paths: [...], tags: [...] }
│
├─ Compare le secret à TEMPS CONSTANT
│ (SHA-256 des deux valeurs + timingSafeEqual)
│ → 401 si différent
│
├─ Pour chaque chemin : revalidatePath()
│ un gabarit dynamique ('/ressources/blog/[slug]')
│ est revalidé avec le type 'page'
│
├─ Pour chaque tag : revalidateTag(tag, 'max')
│
└─ Retour { revalidated: true, paths, tags }
Système de cache :
- Listings et articles en
revalidate = 300(5 min) ; pages éditoriales statiques ; agenda rendu côté client. - Par défaut, sans corps explicite, la route revalide la liste historique : médiathèque, blog et gabarit d'article.
- Le profil de cache
'max'des tags applique une sémantique stale-while-revalidate : après un enregistrement, la toute première visite peut encore afficher l'ancienne métadonnée, la suivante aura la nouvelle. C'est le compromis retenu pour un site public.
Si le webhook est perdu :
- Changement reste invisible jusqu'à la prochaine revalidation (max 5 min).
- LeSite continue de servir le cache ancien : pas d'erreur pour le visiteur.
Si REVALIDATE_SECRET n'est pas configuré ou diverge :
- L'appel du Hub repart en 401, sans aucun effet visible sur le site : le contenu finit par apparaître à l'expiration des 5 minutes.
- C'est un symptôme typique de « j'ai publié, rien ne bouge, puis ça apparaît tout seul ».
Contact et demande entrante
Visiteur remplit le formulaire de contact
│
├─ POST /api/contact (LeSite)
│ │
│ ├─ Garde anti-robots maison (lib/anti-bot.ts) : deux signaux,
│ │ aucun script tiers chargé chez le visiteur
│ │
│ ├─ Limite de débit : 5 envois / 10 min par IP
│ │ (IP lue dans X-Real-IP posé par Nginx)
│ │
│ ├─ Valide le sujet (liste fermée) et les champs requis
│ │
│ └─ POST /api/public/contact-notification (LeHub)
│ En-tête : CONTACT_NOTIFY_SECRET
│
└─ LeHub :
1. INSERT ContactRequest (status='nouveau', source='site')
— les champs de qualification vont dans `context`
2. Accusé de réception au demandeur (Resend)
3. Notification admin (e-mail et/ou Telegram, selon la config)
Ce qui a changé : LeSite n'envoie plus l'e-mail lui-même. Il n'a plus de clé Resend, plus d'adresse destinataire en configuration, et une demande n'est plus un simple e-mail qui peut se perdre dans une boîte : c'est une ligne suivie dans /admin/contact-requests.
Secret partagé : CONTACT_NOTIFY_SECRET, identique des deux côtés.
Si LeHub ne répond pas : la demande n'est pas enregistrée et le visiteur voit une erreur. C'est volontaire : mieux vaut lui demander de réessayer que de lui confirmer un envoi qui n'a laissé aucune trace.
Liste des sujets : recopiée dans les deux dépôts (app/api/contact/route.ts côté LeSite, lib/contact-request.ts côté Hub). Toute valeur ajoutée d'un côté doit l'être de l'autre, sinon la saisie manuelle d'une demande ne propose pas le même choix que le formulaire public.
Flux aucune persistence côté LeSite
Aucune des trois données suivantes n'est jamais stockée localement :
- Registrations : créées en LeHub, jamais en LeSite.
- Articles : éditées en LeHub, fetched at runtime par LeSite.
- Preferences : gérées en LeHub, lues par webhooks.
Le seul état propre au navigateur est le panier d'inscription (lib/cart-context.tsx, non persisté) et l'horodatage du rechargement de build périmé (lib/stale-build.ts, dans le stockage local, pour ne pas boucler sur un rechargement).