Fils de discussion
Des commentaires sur un feedback, stockés à côté de lui et servis par le même endpoint — avec le rôle équipe et les e-mails des relecteurs protégés par votre politique d'accès.
Un feedback porte un fil de commentaires : le relecteur demande « 16 ou 24 px ? », l'équipe répond, et la conversation reste à côté du retour au lieu de partir par e-mail. Les fils sont conservés par votre store et servis par @siteping/server sur l'endpoint que vous avez déjà monté — aucune nouvelle route.
Le widget montre le fil au relecteur et le laisse répondre ; le dashboard le montre à votre équipe, qui y répond et y supprime. La suite de cette page couvre le stockage et l'API HTTP.
Dans le widget
Le fil prolonge le message dans la vue détail d'un feedback, dans le panneau : du plus ancien au plus récent, avec un champ de réponse en dessous. Les réponses de l'équipe portent un badge Équipe.
- Le champ de réponse n'apparaît que si la réponse peut être conservée : l'endpoint annonce
capabilities.commentsdans ses réponses de liste (en modestore: le store implémenteaddComment), et lespermissionsdu feedback ne refusent pas les réponses. Face à un serveur antérieur aux fils — nicapabilities, nicomments— le widget n'affiche aucun fil, et rien ne casse. - Qui répond : le relecteur, résolu comme l'auteur d'un feedback — votre option
identity, sinon celle enregistrée dans ce navigateur, sinon la fenêtre d'identification. La fermer n'envoie rien et ne signale rien ; le brouillon reste. Une réponse du widget est toujours enregistrée enclient. - L'envoi : le bouton ou Ctrl/⌘+Entrée. Les pannes réseau et les 5xx sont réessayées comme pour un feedback, avec le même
clientId: un réessai ne duplique jamais la réponse. Une réponse n'est jamais mise en file pour un chargement ultérieur : si elle échoue encore, le brouillon reste et le fil indique qu'elle n'est pas partie. - Votre code en est informé par
onCommentAddedet l'événementcomment:added(la charge utile : le commentaire enregistré,CommentResponse) ; un échec passe paronErroretfeedback:error, comme tout appel d'API. - Lire la réponse demande la liste : avec une
apiKey, ajoutez"GET"àpublicEndpoints— voir Endpoints publics.
Dans le dashboard
Le tiroir montre le fil du feedback ouvert, et laisse votre équipe répondre et supprimer une fois que vous avez dit à l'inbox qui écrit :
<SitepingInbox
projects="my-project"
endpoint="/api/siteping"
apiKey={KEY}
author={{ name: user.name, email: user.email }}
/>author({ name, email? }) est qui répond depuis cette inbox. Sans lui, les fils sont en lecture seule — et masqués quand ils sont vides. De même quand l'endpoint n'annonce pas de commentaires, ou quand lespermissionsdu feedback refusent les réponses ; une réponse ne propose pas de suppression quand l'endpoint n'annonce pas de suppression, que lespermissionsdu feedback la refusent, ou que l'inbox est enreadOnly.- Les réponses demandent le rôle
team, que le serveur ne garde que pour un appelant dont votre politique d'accès se porte garante — l'apiKey, oucanCommentAsTeamsousaccess(voir le rôle équipe). Toute autre réponse est enregistrée enclient. - Rien n'est optimiste : une réponse s'affiche une fois enregistrée, et un échec garde le brouillon. Supprimer une réponse demande confirmation ; comme tout
DELETE, il faut l'apiKeyou l'accord de votre politique d'accès. - Sans interface :
useSitepingInboxrenvoiecanComment,canDeleteComment,addComment(id, body, clientId?)etdeleteComment(id, commentId)— voir le hook. Une source personnalisée s'y branche avecaddCommentetremoveComment.
Ce que contient un commentaire
| Champ | Type | Notes |
|---|---|---|
id | string | Attribué par le store |
feedbackId | string | Le feedback dont le fil le contient |
body | string | Nettoyé des espaces en bordure, de 1 à 5000 caractères |
authorName | string | De 1 à 200 caractères |
authorEmail | string | Une adresse valide, ou "" quand l'auteur n'en a pas (un utilisateur du dashboard). Masqué comme celui du feedback — voir les e-mails |
authorRole | "client" | "team" | client pour le relecteur sur le site, team pour le côté projet — voir le rôle équipe |
createdAt | chaîne ISO | Quand le store l'a enregistré |
Chaque feedback d'une réponse porte comments, du plus ancien au plus récent : [] quand le fil est vide, ou quand le store ne garde aucun commentaire.
- Des envois idempotents. Un commentaire porte aussi un
clientIdque le client génère. Renvoyer le mêmeclientIdrenvoie le commentaire enregistré au lieu de l'ajouter deux fois : une requête relancée ne duplique jamais une réponse. Comme celui du feedback, il n'est jamais renvoyé. - 100 commentaires par fil (
MAX_COMMENTS_PER_FEEDBACK). Les listes embarquent les fils entiers, qui ne sont pas paginés : la limite borne le poids d'un feedback. - Un commentaire ne touche pas à l'
updatedAtdu feedback.updatedAtsuit les champs propres au feedback (son statut) ; lecreatedAtdu commentaire le plus récent dit quand le fil a bougé pour la dernière fois. - Supprimer un feedback supprime son fil.
API HTTP
Les commentaires partagent l'unique endpoint : un corps de POST avec un feedbackId est un commentaire, un corps de DELETE avec un commentId en supprime un. Les corps propres aux feedbacks ne portent jamais ces clés.
Poster un commentaire
POST /api/siteping
Content-Type: application/json
{
"projectName": "mon-site",
"feedbackId": "cm3f8x…",
"body": "C'est 16 ou 24 px ?",
"authorName": "Alice",
"authorEmail": "alice@client.example",
"clientId": "4f0c2b8e-4c1d-4f7a-9d4e-6b1f2a3c5d7e"
}Il répond 201 avec le commentaire. authorRole est optionnel et vaut client par défaut. clientId est obligatoire — [a-zA-Z0-9_-], jusqu'à 200 caractères : générez-en un par commentaire (crypto.randomUUID()) et renvoyez le même en cas de relance.
Supprimer un commentaire
DELETE /api/siteping
Content-Type: application/json
{ "projectName": "mon-site", "feedbackId": "cm3f8x…", "commentId": "cm4a1k…" }Il répond 200 { "deleted": true }. Comme tout DELETE, il exige l'apiKey — ou ce que demande votre politique d'accès.
Lire les fils
La liste GET ne change pas, à deux ajouts près : les comments de chaque feedback, et un objet capabilities qui dit si le store accepte les commentaires (comments) et sait les supprimer (deleteComments) — un client peut ainsi masquer sa zone de réponse, ou ses boutons de suppression, d'emblée au lieu de tomber sur un 501 :
{
"feedbacks": [{ "id": "cm3f8x…", "message": "…", "comments": [] }],
"total": 1,
"capabilities": { "comments": true, "deleteComments": true }
}Erreurs
| Statut | Quand |
|---|---|
400 { errors } | Le corps échoue à la validation (texte vide, e-mail invalide, clientId absent…) |
401 / 403 | Identifiants absents, ou refus d'authorize ou d'un contrôle CSRF |
404 { error: "Feedback not found" } | Feedback inconnu, ou d'un autre projet |
404 { error: "Comment not found" } | Le commentaire n'est pas dans le fil de ce feedback |
409 { error } | Le fil contient déjà 100 commentaires, ou le clientId est déjà utilisé sur un autre feedback |
501 { error: "Comments are not supported by this store" } | Le store ne garde aucun commentaire — voir activer les fils |
Le rôle équipe
Le widget poste depuis des navigateurs anonymes : l'authorRole qu'envoie une requête n'est donc qu'une prétention. Le handler ne garde team que lorsque votre politique d'accès se porte garante de l'appelant, et marque tout autre commentaire client :
- Avec la politique
apiKey, la requête doit porterAuthorization: Bearer <apiKey>. LePOSTpublic, une mauvaise clé et un handler sansapiKeyobtiennent tousclient. Un widget configuré avecapiKeyenvoie cette clé depuis le navigateur de chaque visiteur, qui pourrait alors poster au nom de l'équipe : gardezapiKeyhors des widgets publics. - Avec
access, c'estcanCommentAsTeam(principal)qui décide. Sans lui, c'est votre réponse àcanReadAuthorEmail— qui peut lire les e-mails des relecteurs est du côté projet —, donc une politique qui distingue déjà les visiteurs n'a rien à ajouter. Une politique sans aucun des deux marque chaque commentaireclient: écrire en tant qu'équipe permet de parler au nom de votre agence dans le fil, ce droit n'est donc jamais accordé par défaut.
authorize voit deux actions de plus : createComment et deleteComment, avec le feedbackId — et le commentId pour une suppression. Lors d'un dryRun, qui remplit les permissions du feedback, deleteComment arrive sans commentId et sa réponse vaut pour tout le fil : protégez une règle par auteur avec if (!commentId) return false, qui masque les boutons de suppression tandis que le vrai DELETE passe toujours par votre règle.
createSitepingHandler({
store,
access: {
authenticate: (request) => getSessionUser(request),
authorize: ({ principal, action }) =>
action === "deleteComment" ? principal.isStaff : true,
canReadAuthorEmail: (principal) => principal.isStaff,
// Optionnel ici : il reprendrait canReadAuthorEmail.
canCommentAsTeam: (principal) => principal.isStaff,
},
});E-mails des relecteurs
L'authorEmail d'un commentaire est masqué exactement comme celui du feedback : vide sauf si le demandeur peut le lire — l'apiKey en Bearer, ou canReadAuthorEmail avec access. La réponse à un POST de commentaire renvoie à son auteur l'e-mail du nouveau commentaire, comme le fait un POST de feedback ; un POST de feedback rejoué ne renvoie que l'e-mail de son auteur, jamais ceux du fil.
Endpoints publics et abus
- Lire les réponses depuis le widget. Avec une
apiKey,publicEndpointsvaut par défaut["POST", "OPTIONS"]:GETexige la clé, donc un widget public peut poster un commentaire mais pas lire la réponse. Ajoutez"GET"àpublicEndpointspour que les relecteurs lisent leurs fils — les e-mails restent masqués. - Limitation de débit. Le
POSTpublic accepte aussi les commentaires : quiconque connaît l'id d'un feedback et le nom de votre projet peut en ajouter. Limitez le débit dePOSTdans votre framework ou votre reverse proxy ; la limite de 100 commentaires borne la taille d'un fil, pas sa vitesse de remplissage. - Notifications. Les webhooks et
onCreatedse déclenchent pour les nouveaux feedbacks seulement, pas pour les commentaires.
Activer les fils sur votre store
| Store | Quoi faire |
|---|---|
| Prisma | Lancez npx @siteping/cli sync pour ajouter le modèle SitepingComment, puis npx prisma db push (ou npx prisma migrate dev). Tant que le client n'est pas généré avec lui, les fils se lisent vides et les écritures de commentaires répondent 501 |
| Drizzle | Exportez sitepingComments depuis votre schéma à côté des autres tables, puis générez et appliquez la migration avec drizzle-kit |
| Memory, localStorage | Rien — les deux gardent les fils. Les données localStorage enregistrées par une version antérieure se lisent avec des fils vides |
| Le vôtre | Implémentez les méthodes optionnelles addComment et deleteComment — createCollectionStore fournit les deux |
Serveur
Un endpoint HTTP devant n'importe quel store, pour n'importe quel framework — auth, CORS, validation, redaction, hooks et webhooks sur la Fetch API.
Choisir un adapter
Où vivent vos feedbacks — Prisma ou Drizzle pour la production, memory pour les tests et démos, localStorage pour le tout-client.