FastComments.com

Ajouter les 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

If your val already knows who the visitor is, Secure SSO hands that identity to the widget so they never see a second login. There are no endpoints to build and nothing to call at runtime: you compute three values server-side and pass them in the widget config.

Val Town ships zero-config login with std/oauth, so the visitor can sign in with the Val Town account they already have. Swap that for whatever your app uses; the FastComments half does not change.

Build the payload on the server

The API secret signs the payload and must never reach browser code. Install the SDK from npm, which works on Val Town's Deno runtime as-is:

sso.ts
Copy CopyRun External Link
1
2import { SecureSSOPayloadBuilder } from "npm:fastcomments-sdk/server";
3
4export function buildSSOPayload(user) {
5 // l'identifiant doit être stable pour la même personne, sinon ils obtiennent une nouvelle identité de commentaire à chaque connexion.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // l'email est requis et doit être unique.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // le nom d'utilisateur est requis et ne peut pas être un email.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() returns { userDataJSONBase64, verificationHash, timestamp }. Those three values are all that reach the browser. The secret signs them and is then dropped, so nothing in the page lets a reader forge a different user.

Pass it to the 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 // ...rendre le widget avec cette configuration
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware adds GET /auth/login, GET /auth/callback and POST /auth/logout for you. Note that logout is a POST, while the widget navigates to logoutURL with a GET, so point logoutURL at a small route of your own that submits the POST.

When the visitor is logged out, pass sso with only a loginURL. The widget then shows a login prompt instead of an anonymous comment box.

Things that go wrong

timestamp is epoch milliseconds, must not be in the future, and must not be more than two days old. Generate it on the server in the same request that computes the hash. Generating it in the browser is the classic failure: the value differs from the one that was hashed and every comment is rejected.

Never set isAdmin or isModerator from the identity provider. Signing in with a Val Town account says nothing about who should moderate your site.

See the SSO guide for the full field list, group-gated threads, and 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.