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.

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

On an index page, don't render one comment-count widget per row. That is one request per post. Use the bulk count, which takes a single request for the whole page.

Mark each row with the urlId its thread uses, then load the bulk widget once:

Recuentos de comentarios en bloque 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

The script finds every .fast-comments-count element on the page and fills in its count.

data-fast-comments-url-id has to match the urlId that post's comment widget uses. If the widget uses the slug, the marker uses the slug. A mismatch shows zero on a thread that has comments.

The script polls for window.FastCommentsBulkCountConfig, so it does not matter whether you set the config before or after the script tag.

SSO Seguro 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 // el id debe ser estable para la misma persona, o recibirán una nueva identidad de comentario en cada inicio de sesión.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // el correo electrónico es obligatorio y debe ser único.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // el nombre de usuario es obligatorio y no puede ser un correo electrónico.
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

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 // ...renderizar el widget con esta configuración
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.

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.

Vals de ejemplo 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.