FastComments.com

Aggiungi commenti live alle app Val Town

Val Town esegue TypeScript su Deno, quindi un val è un server reale. Questo lo rende adatto a FastComments: il widget è un tag script nella pagina, e tutto ciò che richiede un segreto, come Secure SSO o la verifica di un webhook, può essere eseguito lato server nello stesso val.

Questa guida copre l'aggiunta del widget dei commenti a un val HTTP, la visualizzazione dei conteggi dei commenti su una pagina indice, l'accesso degli utenti con l'account Val Town che già possiedono, e la ricezione dei webhook dei commenti.

Non è necessario un account per provarlo. Gli esempi usano tenantId: "demo", un sandbox condiviso, e il Passo 2 spiega come passare al proprio.

Conteggi dei commenti su una pagina indice Internal Link

Su una pagina indice, non visualizzare un widget di conteggio dei commenti per riga. Questo comporta una richiesta per ogni post. Usa il conteggio in blocco, che richiede una singola richiesta per l'intera pagina.

Marca ogni riga con il urlId che utilizza il suo thread, quindi carica il widget in blocco una sola volta:

Conteggi dei commenti in blocco su una pagina indice
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

Lo script trova ogni elemento .fast-comments-count nella pagina e ne riempie il conteggio.

data-fast-comments-url-id deve corrispondere al urlId utilizzato dal widget dei commenti del post. Se il widget utilizza lo slug, il marcatore utilizza lo slug. Una mancata corrispondenza mostra zero su un thread che ha commenti.

Lo script controlla window.FastCommentsBulkCountConfig, quindi non importa se imposti la configurazione prima o dopo il tag script.

SSO sicuro con 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'id deve essere stabile per la stessa persona, altrimenti ottengono una nuova identità di commento ad ogni login.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // l'email è obbligatoria e deve essere unica.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // il nome utente è obbligatorio e non può essere 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

Configurazione del widget con 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 // ...renderizza il widget con questa configurazione
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.

Ricezione dei webhook Internal Link

A val è un ricevitore webhook naturale: ha un URL stabile, può verificare una firma e include SQLite e storage blob integrati.

FastComments firma ${timestamp}.${body} con il segreto API del tuo account e invia due intestazioni:

Intestazioni webhook
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

Il metodo trasporta l'evento: PUT per un commento creato o aggiornato, DELETE per uno eliminato.

Verifica di una consegna
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // The exact bytes that arrived. Do NOT use c.req.json() and re-serialize.
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 // Reject stale deliveries so a captured request cannot be replayed later.
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 // ...handle JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Due cose che mordono

Verifica i byte grezzi. L'analisi del JSON e la sua riserializzazione cambiano l'ordine delle chiavi e gli spazi, quindi l'hash differisce e ogni consegna fallisce senza una causa evidente. Questo è il motivo più comune per cui un ricevitore webhook "semplicemente non funziona".

Confronta in tempo costante. Un semplice === sulla firma rivela quanti byte corrispondono, il che è sufficiente per forgiarne uno alla volta.

Gestione degli eventi

Rispondi rapidamente. FastComments riprova in caso di risposta non 2xx, e un endpoint che continua a fallire viene disabilitato automaticamente, quindi esegui il lavoro reale dopo aver risposto anziché inline.

Rendi quel lavoro idempotente sull'ID del commento. Un retry viene firmato nuovamente con un timestamp nuovo, e lo stesso ID del commento arriva di nuovo in caso di modifica o eliminazione, quindi non c'è nulla di stabile su cui deduplicare.


Esempi di Vals Internal Link

Quattro vals pubblici che puoi remixare, ognuno coprendo una parte di questa guida.

Blog con commenti (live) è un blog Markdown con un thread sotto ogni post e conteggi di commenti in blocco nella pagina indice. Funziona nel momento in cui lo remixi, e una variabile d'ambiente lo punta al tuo account.

Demo SSO (live) accede il visitatore con il suo account Val Town e passa quell'identità al widget, così non c'è un secondo login.

Ricevitore Webhook (live) verifica la firma HMAC su ogni consegna e memorizza gli eventi in SQLite. Ha un pulsante che firma un payload di test e lo invia a se stesso, così puoi vedere la verifica riuscire prima di configurare un webhook reale.

Competenze agente (live) è una libreria di competenze dell'agente FastComments che copre il widget, SSO, la REST API, la moderazione e la migrazione da Disqus. Remixala e l'agente di Val Town, Townie, carica automaticamente le competenze dalla cartella skills/, così il tuo agente sa come integrare i commenti senza che tu debba incollare la documentazione nella chat.

Le stesse competenze si installano ovunque altro con npx skills add fastcomments/skills.