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

Se il tuo val già conosce l'identità del visitatore, Secure SSO passa quell'identità al widget, così non vedrà mai un secondo login. Non ci sono endpoint da costruire e nulla da chiamare a runtime: calcoli tre valori lato server e li passi nella configurazione del widget.

Val Town offre login zero-config con std/oauth, quindi il visitatore può accedere con l'account Val Town che già possiede. Sostituiscilo con quello che usa la tua app; la parte FastComments non cambia.

Costruisci il payload sul server

L'API secret firma il payload e non deve mai finire nel codice del browser. Installa l'SDK da npm, che funziona sul runtime Deno di Val Town così com'è:

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() restituisce { userDataJSONBase64, verificationHash, timestamp }. Quei tre valori sono tutto ciò che arriva al browser. Il secret li firma e viene poi scartato, quindi nulla nella pagina permette a un lettore di falsificare un utente diverso.

Passalo al 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 // ...render the widget with this config
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware aggiunge per te GET /auth/login, GET /auth/callback e POST /auth/logout. Nota che il logout è una POST, mentre il widget naviga verso logoutURL con una GET, quindi punta logoutURL a una piccola route tua che invia la POST.

Quando il visitatore non è autenticato, passa sso con solo un loginURL. Il widget mostrerà quindi un prompt di login invece della casella dei commenti anonima.

Cose che vanno storte

timestamp è in millisecondi epoch, non deve essere nel futuro e non deve essere più vecchio di due giorni. Genera lato server nella stessa richiesta che calcola l'hash. Generarlo nel browser è l'errore classico: il valore differisce da quello firmato e ogni commento viene rifiutato.

Non impostare mai isAdmin o isModerator dall'identity provider. Accedere con un account Val Town non dice nulla su chi dovrebbe moderare il tuo sito.

Consulta la guida SSO per l'elenco completo dei campi, i thread con accesso a gruppi e i badge.

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.