FastComments.com

Ajouter des commentaires en direct aux applications Val Town

Val Town exécute TypeScript sur Deno, donc un val est un vrai serveur. Cela en fait un bon choix pour FastComments : le widget est une balise script sur la page, et tout ce qui nécessite un secret, comme le SSO sécurisé ou la vérification d’un webhook, peut s’exécuter côté serveur dans le même val.

Ce guide couvre l’ajout du widget de commentaires à un val HTTP, l’affichage du nombre de commentaires sur une page d’index, la connexion des utilisateurs avec le compte Val Town qu’ils possèdent déjà, et la réception des webhooks de commentaires.

Vous n’avez pas besoin de compte pour l’essayer. Les exemples utilisent tenantId: "demo", un bac à sable partagé, et l’étape 2 explique comment passer au vôtre.

Nombre de commentaires sur une page d'index Internal Link

Sur une page d'index, n'affichez pas un widget de compteur de commentaires par ligne. Cela représente une requête par article. Utilisez le comptage en masse, qui ne nécessite qu'une seule requête pour toute la page.

Marquez chaque ligne avec le urlId utilisé par son fil, puis chargez le widget en masse une seule fois :

Comptes de commentaires en masse sur un index
Copy CopyRun External Link
1
2<ul>
3 {posts.map((post) => (
4 <li>
5 <a href={post.slug}>{post.title}</a>{" "}
6 <span class="fast-comments-count" data-fast-comments-url-id={post.slug}></span>
7 </li>
8 ))}
9</ul>
10
11<script
12 dangerouslySetInnerHTML={{
13 __html: `window.FastCommentsBulkCountConfig = ${
14 JSON.stringify({ tenantId: TENANT_ID })
15 };`,
16 }}
17/>
18<script src="https://cdn.fastcomments.com/js/embed-widget-comment-count-bulk.min.js"></script>
19

Le script trouve chaque élément .fast-comments-count sur la page et remplit son compteur.

data-fast-comments-url-id doit correspondre au urlId utilisé par le widget de commentaires du post. Si le widget utilise le slug, le marqueur utilise le slug. Un décalage affiche zéro sur un fil qui possède des commentaires.

Le script interroge window.FastCommentsBulkCountConfig, il n'importe donc pas si vous définissez la configuration avant ou après la balise script.

SSO sécurisé avec std/oauth Internal Link

Si votre val connaît déjà l'identité du visiteur, Secure SSO transmet cette identité au widget afin qu'il ne voie jamais un second écran de connexion. Il n'y a aucun endpoint à construire et rien à appeler à l'exécution : vous calculez trois valeurs côté serveur et les passez dans la configuration du widget.

Val Town propose une connexion sans configuration avec std/oauth, afin que le visiteur puisse se connecter avec le compte Val Town qu'il possède déjà. Remplacez cela par ce que votre application utilise ; la partie FastComments ne change pas.

Construire le payload côté serveur

Le secret d'API signe le payload et ne doit jamais atteindre le code du navigateur. Installez le SDK depuis npm, qui fonctionne tel quel sur le runtime Deno de Val Town :

sso.ts
Copy CopyRun External Link
1
2import { SecureSSOPayloadBuilder } from "npm:fastcomments-sdk/server";
3
4export function buildSSOPayload(user) {
5 // id must be stable for the same person, or they get a new comment identity on every login.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email is required and must be unique.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // username is required and cannot be an email.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() renvoie { userDataJSONBase64, verificationHash, timestamp }. Ces trois valeurs sont tout ce qui atteint le navigateur. Le secret les signe puis est abandonné, de sorte que rien dans la page ne permet à un lecteur de falsifier un autre utilisateur.

Le passer au widget

Configuration du widget avec SSO
Copy CopyRun External Link
1
2import { getOAuthUserData, oauthMiddleware } from "https://esm.town/v/std/oauth/middleware.ts";
3
4app.get("/", async (c) => {
5 const session = await getOAuthUserData(c.req.raw);
6 const user = session?.user;
7
8 const config = {
9 tenantId: TENANT_ID,
10 urlId: "my-thread",
11 ...(user
12 ? { sso: { ...buildSSOPayload(user), logoutURL: "/logout" } }
13 : { sso: { loginURL: "/auth/login" } }),
14 };
15
16 // ...render the widget with this config
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware ajoute GET /auth/login, GET /auth/callback et POST /auth/logout pour vous. Notez que la déconnexion est un POST, tandis que le widget navigue vers logoutURL avec un GET ; faites donc pointer logoutURL vers une petite route de votre cru qui soumet le POST.

Lorsque le visiteur est déconnecté, passez sso avec uniquement un loginURL. Le widget affiche alors une invite de connexion au lieu d'une boîte de commentaires anonyme.

Ce qui peut mal tourner

timestamp est en millisecondes epoch, ne doit pas être dans le futur et ne doit pas dater de plus de deux jours. Générez-le côté serveur dans la même requête que celle qui calcule le hash. Le générer dans le navigateur est l'erreur classique : la valeur diffère de celle qui a été signée et chaque commentaire est rejeté.

Ne définissez jamais isAdmin ou isModerator à partir du fournisseur d'identité. Se connecter avec un compte Val Town ne dit rien de qui devrait modérer votre site.

Consultez le guide SSO pour la liste complète des champs, les fils restreints par groupe et les badges.

Réception des webhooks Internal Link

A val est un récepteur de webhook naturel : il possède une URL stable, il peut vérifier une signature, et il intègre SQLite et le stockage de blobs.

FastComments signe ${timestamp}.${body} avec le secret API de votre compte et envoie deux en‑têtes :

En‑têtes du webhook
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

La méthode transporte l'événement : PUT pour un commentaire créé ou mis à jour, DELETE pour un commentaire supprimé.

Vérification d'une livraison
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Les octets exacts reçus. N'utilisez PAS c.req.json() et ne les re‑sérialisez pas.
6 const rawBody = await c.req.raw.text();
7 const timestamp = c.req.raw.headers.get("X-FastComments-Timestamp");
8 const signature = c.req.raw.headers.get("X-FastComments-Signature");
9
10 if (!timestamp || !signature) return new Response("Missing headers", { status: 400 });
11
12 // Rejeter les livraisons obsolètes afin qu'une requête capturée ne puisse pas être rejouée plus tard.
13 if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) {
14 return new Response("Timestamp outside window", { status: 400 });
15 }
16
17 const expected = "sha256=" + createHmac("sha256", Deno.env.get("FASTCOMMENTS_API_SECRET"))
18 .update(`${timestamp}.${rawBody}`)
19 .digest("hex");
20
21 const a = new TextEncoder().encode(signature);
22 const b = new TextEncoder().encode(expected);
23 if (a.length !== b.length || !timingSafeEqual(a, b)) {
24 return new Response("Signature mismatch", { status: 401 });
25 }
26
27 // ...traiter JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Deux choses qui posent problème

Vérifier les octets bruts. Analyser le JSON et le re‑sérialiser modifie l'ordre des clés et les espaces, ainsi le hachage diffère et chaque livraison échoue sans cause évidente. C’est la raison habituelle pour laquelle un récepteur de webhook « ne fonctionne tout simplement pas ».

Comparer en temps constant. Un simple === sur la signature révèle le nombre d'octets correspondants, ce qui suffit à falsifier un octet à la fois.

Gestion des événements

Répondez rapidement. FastComments réessaye en cas de réponse non‑2xx, et un point de terminaison qui échoue continuellement est finalement désactivé automatiquement, il faut donc effectuer le vrai travail après avoir répondu plutôt qu’en ligne.

Rendez cela idempotent sur l'ID du commentaire. Un nouveau essai est re‑signé avec un nouveau horodatage, et le même ID de commentaire arrive de nouveau lors d'une modification ou d'une suppression, il n’y a donc rien de stable sur quoi dédupliquer.

Exemples de Vals Internal Link

Quatre vals publics que vous pouvez remixer, chacun couvrant une partie de ce guide.

Blog avec commentaires (en direct) est un blog Markdown avec un fil de discussion sous chaque article et des comptes de commentaires en masse sur l'index. Il fonctionne dès que vous le remixer, et une variable d'environnement le pointe vers votre propre compte.

Démo SSO (en direct) connecte le visiteur avec son compte Val Town et transmet cette identité au widget, de sorte qu'il n'y ait pas de deuxième connexion.

Récepteur de webhook (en direct) vérifie la signature HMAC à chaque livraison et stocke les événements dans SQLite. Il possède un bouton qui signe une charge utile de test et la délivre à lui-même, vous permettant de voir la vérification réussir avant de configurer un vrai webhook.

Compétences d'agent (en direct) est une bibliothèque de compétences d'agent FastComments couvrant le widget, le SSO, l'API REST, la modération et la migration depuis Disqus. Remixez-le et l'agent de Val Town, Townie, récupère automatiquement les compétences dans skills/, de sorte que votre agent sache comment configurer les commentaires sans que vous ayez à coller la documentation dans le chat.

Les mêmes compétences s'installent partout ailleurs avec npx skills add fastcomments/skills.