FastComments.com

Adicionar Comentários ao Vivo aos Aplicativos Val Town


Val Town executa TypeScript no Deno, então um val é um servidor real. Isso o torna uma boa escolha para o FastComments: o widget é uma tag script na página, e qualquer coisa que precise de um segredo, como Secure SSO ou verificação de webhook, pode ser executada no lado do servidor no mesmo val.

Este guia cobre a adição do widget de comentários a um val HTTP, a exibição de contagens de comentários em uma página de índice, o login de usuários com a conta Val Town que eles já possuem e o recebimento de webhooks de comentários.

Você não precisa de uma conta para experimentá-lo. Os exemplos usam tenantId: "demo", um sandbox compartilhado, e o Passo 2 cobre a troca para o seu próprio.

Contagem de Comentários em uma Página de Índice Internal Link

Em uma página de índice, não renderize um widget de contagem de comentários por linha. Isso gera uma solicitação por postagem. Use a contagem em massa, que faz uma única solicitação para a página inteira.

Marque cada linha com o urlId que seu thread usa e, em seguida, carregue o widget em massa uma única vez:

Contagens de comentários em massa em um í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

O script encontra cada elemento .fast-comments-count na página e preenche sua contagem.

data-fast-comments-url-id deve corresponder ao urlId que o widget de comentários da postagem usa. Se o widget usar o slug, o marcador usará o slug. Uma incompatibilidade exibirá zero em um thread que tem comentários.

O script verifica window.FastCommentsBulkCountConfig, portanto não importa se você definir a configuração antes ou depois da tag de script.

SSO Seguro com 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 // id must be stable for the same person, or they get a new comment identity on every login.
6 // id deve ser estável para a mesma pessoa, ou eles receberão uma nova identidade de comentário a cada login.
7 const id = `vt-${user.id}`;
8
9 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
10 id,
11 // email is required and must be unique.
12 // email é obrigatório e deve ser único.
13 email: user.email ?? `${id}@users.noreply.val.town`,
14 // username is required and cannot be an email.
15 // username é obrigatório e não pode ser um email.
16 username: user.username ?? id,
17 displayName: user.username ?? undefined,
18 avatar: user.links.profileImageUrl ?? undefined,
19 }).getPayload();
20}
21

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

Configuração do Widget com 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 // ...renderizar o widget com esta configuração
18});
19
20export default oauthMiddleware(app.fetch);
21

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.

Recebendo Webhooks Internal Link

Um val é um receptor de webhook natural: ele tem uma URL estável, pode verificar uma assinatura e inclui SQLite e armazenamento de blobs embutidos.

FastComments assina ${timestamp}.${body} com o segredo da API da sua conta e envia dois cabeçalhos:

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

O método transporta o evento: PUT para um comentário criado ou atualizado, DELETE para um comentário excluído.

Verificando uma entrega
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Os bytes exatos que chegaram. NÃO use c.req.json() e reserialize.
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 // Rejeitar entregas antigas para que uma solicitação capturada não possa ser reproduzida mais tarde.
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 // ...tratar JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Duas coisas que dão dor

Verifique os bytes brutos. Analisar o JSON e reserializá‑lo altera a ordem das chaves e os espaços em branco, portanto o hash difere e cada entrega falha sem causa óbvia. Essa é a razão usual de um receptor de webhook "simplesmente não funciona".

Compare em tempo constante. Um simples === na assinatura vaza quantos bytes coincidem, o que é suficiente para forjar um byte de cada vez.

Manipulando eventos

Responda rapidamente. FastComments tenta novamente em caso de resposta não‑2xx, e um endpoint que continua falhando é eventualmente desativado automaticamente, portanto faça o trabalho real após responder, em vez de inline.

Torne esse trabalho idempotente com base no ID do comentário. Uma nova tentativa é assinada novamente com um timestamp novo, e o mesmo ID de comentário chega novamente em edição e exclusão, portanto não há nada estável para desduplicar.


Exemplos de Vals Internal Link


Quatro vals públicos que você pode remixar, cada um cobrindo uma parte deste guia.

Blog com comentários (live) é um blog em Markdown com um tópico sob cada post e contagens de comentários em massa no índice. Ele funciona no momento em que você o remixar, e uma variável de ambiente aponta para sua própria conta.

Demo SSO (live) autentica o visitante com a conta do Val Town dele e entrega essa identidade ao widget, de modo que não há um segundo login.

Receptor de Webhook (live) verifica a assinatura HMAC em cada entrega e armazena os eventos em SQLite. Ele possui um botão que assina uma carga de teste e a entrega a si mesmo, para que você possa observar a verificação ser bem‑sucedida antes de configurar um webhook real.

Habilidades de agente (live) é uma biblioteca de habilidades de agente FastComments que cobre o widget, SSO, a API REST, moderação e migração do Disqus. Remix‑a e o agente da Val Town, Townie, carrega as habilidades de skills/ automaticamente, de modo que seu agente saiba como integrar comentários sem que você precise colar a documentação no chat.

As mesmas habilidades podem ser instaladas em qualquer outro lugar com npx skills add fastcomments/skills.