FastComments.com

Agregar Comentarios en Vivo a las Aplicaciones de Val Town

Val Town ejecuta TypeScript en Deno, por lo que un val es un servidor real. Eso lo hace adecuado para FastComments: el widget es una etiqueta script en la página, y cualquier cosa que necesite un secreto, como Secure SSO o la verificación de un webhook, puede ejecutarse del lado del servidor en el mismo val.

Esta guía cubre cómo agregar el widget de comentarios a un val HTTP, mostrar recuentos de comentarios en una página de índice, iniciar sesión de usuarios con la cuenta de Val Town que ya poseen, y recibir webhooks de comentarios.

No necesitas una cuenta para probarlo. Los ejemplos usan tenantId: "demo", un sandbox compartido, y el Paso 2 cubre cómo cambiar a tu propio.

Recuento de Comentarios en una Página de Índice Internal Link

En una página de índice, no renderices un widget de recuento de comentarios por fila. Eso es una solicitud por publicación. Usa el recuento masivo, que realiza una única solicitud para toda la página.

Marca cada fila con el urlId que usa su hilo, y luego carga el widget masivo una sola vez:

Recuentos masivos de comentarios en un índice
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

El script busca cada elemento .fast-comments-count en la página y completa su recuento.

data-fast-comments-url-id debe coincidir con el urlId que usa el widget de comentarios de esa publicación. Si el widget usa el slug, el marcador usa el slug. Una discrepancia muestra cero en un hilo que tiene comentarios.

El script sondea window.FastCommentsBulkCountConfig, por lo que no importa si configuras la configuración antes o después de la etiqueta del script.

SSO Seguro con std/oauth Internal Link

Si tu val ya sabe quién es el visitante, Secure SSO le pasa esa identidad al widget para que nunca vea un segundo inicio de sesión. No hay endpoints que construir ni nada que llamar en tiempo de ejecución: calculas tres valores en el servidor y los pasas en la configuración del widget.

Val Town incluye inicio de sesión sin configuración con std/oauth, así que el visitante puede iniciar sesión con la cuenta de Val Town que ya tiene. Cámbialo por el que use tu aplicación; la parte de FastComments no cambia.

Construye el payload en el servidor

El API secret firma el payload y nunca debe llegar al código del navegador. Instala el SDK desde npm, que funciona en el runtime Deno de Val Town tal cual:

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() devuelve { userDataJSONBase64, verificationHash, timestamp }. Esos tres valores son todo lo que llega al navegador. El secret los firma y luego se descarta, así que nada en la página permite a un lector suplantar a otro usuario.

Pásalo al widget

Configuración 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 añade GET /auth/login, GET /auth/callback y POST /auth/logout por ti. Ten en cuenta que el logout es un POST, mientras que el widget navega a logoutURL con un GET, así que apunta logoutURL a una pequeña ruta propia que envíe el POST.

Cuando el visitante ha cerrado sesión, pasa sso con solo un loginURL. El widget mostrará entonces un mensaje de inicio de sesión en lugar de un cuadro de comentarios anónimo.

Cosas que salen mal

timestamp está en milisegundos de época, no debe estar en el futuro y no debe tener más de dos días de antigüedad. Genéralo en el servidor en la misma petición que calcula el hash. Generarlo en el navegador es el fallo clásico: el valor difiere del que se firmó con el hash y cada comentario es rechazado.

Nunca establezcas isAdmin o isModerator a partir del proveedor de identidad. Iniciar sesión con una cuenta de Val Town no dice nada sobre quién debería moderar tu sitio.

Consulta la guía de SSO para ver la lista completa de campos, los hilos restringidos por grupos y las insignias.

Recibiendo Webhooks Internal Link

A val es un receptor de webhook natural: tiene una URL estable, puede verificar una firma y tiene SQLite y almacenamiento de blobs incorporados.

FastComments firma ${timestamp}.${body} con el secreto API de tu cuenta y envía dos encabezados:

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

El método lleva el evento: PUT para un comentario creado o actualizado, DELETE para uno eliminado.

Verificando una entrega
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

Dos cosas que muerden

Verifica los bytes crudos. Analizar el JSON y volver a serializarlo cambia el orden de las claves y los espacios en blanco, por lo que el hash difiere y cada entrega falla sin una causa evidente. Esta es la razón habitual por la que un receptor de webhook "simplemente no funciona".

Compara en tiempo constante. Un simple === sobre la firma revela cuántos bytes coinciden, lo que es suficiente para falsificar un byte a la vez.

Manejo de eventos

Responde rápidamente. FastComments reintenta en caso de una respuesta que no sea 2xx, y un endpoint que sigue fallando se desactiva automáticamente al final, así que realiza el trabajo real después de responder en lugar de hacerlo en línea.

Haz que ese trabajo sea idempotente respecto al id del comentario. Un reintento se vuelve a firmar con una marca de tiempo nueva, y el mismo id de comentario llega de nuevo en la edición y eliminación, por lo que no hay nada estable sobre lo que deduplicar.

Ejemplo Vals Internal Link

Cuatro vals públicos que puedes remezclar, cada uno cubriendo una parte de esta guía.

Blog con comentarios (live) es un blog en Markdown con un hilo bajo cada publicación y recuentos de comentarios en bloque en el índice. Funciona en el momento en que lo remezclas, y una variable de entorno lo apunta a tu propia cuenta.

Demo SSO (live) inicia sesión al visitante con su cuenta de Val Town y entrega esa identidad al widget, por lo que no hay un segundo inicio de sesión.

Receptor de Webhook (live) verifica la firma HMAC en cada entrega y almacena los eventos en SQLite. Tiene un botón que firma una carga de prueba y la entrega a sí mismo, para que puedas observar la verificación exitosa antes de configurar un webhook real.

Habilidades de agente (live) es una biblioteca de habilidades de agente de FastComments que cubren el widget, SSO, la API REST, la moderación y la migración desde Disqus. Remezcla esto y el agente de Val Town, Townie, recoge automáticamente las habilidades de skills/, de modo que tu agente sepa cómo integrar los comentarios sin que tengas que pegar la documentación en el chat.

Las mismas habilidades se instalan en cualquier otro lugar con npx skills add fastcomments/skills.