FastComments.com

Dodajte live komentarisanje u aplikacije Val Town

Val Town pokreće TypeScript na Deno, pa je val pravi server. To ga čini dobrim izborom za FastComments: widget je script tag na stranici, a sve što zahteva tajnu, poput Secure SSO ili verifikacije webhook‑a, može se izvršavati na serveru u istom val‑u.

Ovaj vodič pokriva dodavanje widgeta za komentare u HTTP val, prikazivanje broja komentara na indeksnoj stranici, prijavljivanje korisnika pomoću Val Town naloga koji već imaju, i primanje webhook‑ova za komentare.

Ne morate imati nalog da biste ga isprobali. Primeri koriste tenantId: "demo", zajednički sandbox, a korak 2 objašnjava kako preći na svoj.

Broj komentara na indeksnoj stranici Internal Link

Na indeksnoj stranici, ne prikazujte po jedan widget za brojanje komentara po redu. To je jedan zahtev po objavi. Koristite grupno brojanje, koje zahteva jedan zahtev za celu stranicu.

Označite svaki red sa urlId koji njegova nit koristi, zatim učitajte bulk widget jednom:

Grupno brojanje komentara na indeksnoj stranici
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

Skript pronalazi svaki element .fast-comments-count na stranici i popunjava njegov broj.

data-fast-comments-url-id mora da se podudara sa urlId koji widget za komentare objave koristi. Ako widget koristi slug, marker koristi slug. Nepodudaranje prikazuje nulu na niti koja ima komentare.

Skript periodično proverava window.FastCommentsBulkCountConfig, pa nije važno da li konfiguraciju postavite pre ili posle <script> taga.

Sigurni SSO sa 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 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() 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

Konfiguracija widgeta sa 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 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.

Primanje webhook-ova Internal Link

A val je prirodni webhook prijemnik: ima stabilan URL, može verifikovati potpis i ima ugrađenu SQLite i blob skladište.

FastComments potpisuje ${timestamp}.${body} tajnim ključem API-ja vašeg naloga i šalje dva zaglavlja:

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

Metoda nosi događaj: PUT za kreiran ili ažuriran komentar, DELETE za obrisan komentar.

Verifikacija isporuke
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Tačni bajtovi koji su stigli. Nemojte koristiti c.req.json() i ponovo serijalizovati.
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 // Odbaci zastarele isporuke kako uhvaćeni zahtev ne bi mogao biti ponovo reprodukovan kasnije.
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 // ...obradi JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Dve stvari koje mogu da zagrizu

Verifikujte sirove bajtove. Parsiranje JSON-a i njegovo ponovo serijalizovanje menja redosled ključeva i razmake, pa se hash razlikuje i svaka isporuka ne uspeva bez očiglednog uzroka. Ovo je uobičajen razlog da webhook prijemnik „jednostavno ne radi“.

Uporedite u konstantnom vremenu. Obična === operacija na potpisu otkriva koliko bajtova se podudara, što je dovoljno da se falsifikuje po jedan bajt odjednom.

Obrada događaja

Odgovorite brzo. FastComments ponovo pokušava na ne‑2xx odgovoru, a krajnja tačka koja stalno ne uspeva se na kraju automatski onemogućava, zato obavite stvarni rad nakon odgovora, a ne inline.

Učinite da rad bude idempotentan po ID‑u komentara. Ponovni pokušaj se ponovo potpisuje svežim timestamp‑om, a isti ID komentara dolazi ponovo pri izmeni i brisanju, pa nema stabilnog podatka za deduplikaciju.

Primer vrednosti Internal Link

Četiri javna vala koja možete remixovati, svako pokriva jedan deo ovog vodiča.

Blog sa komentarima (uživo) je Markdown blog sa nitima ispod svakog posta i grupnim brojem komentara na indeksu. Radi odmah kada ga remixujete, a jedna promenljiva okruženja usmerava ga na vaš sopstveni nalog.

SSO demo (uživo) prijavljuje posetioca pomoću njihovog Val Town naloga i predaje taj identitet widgetu, tako da nema drugog prijavljivanja.

Webhook prijemnik (uživo) verifikuje HMAC potpis na svakoj isporuci i čuva događaje u SQLite. Ima dugme koje potpisuje testni payload i šalje ga samom sebi, tako da možete videti da verifikacija uspe pre podešavanja pravog webhook‑a.

Veštine agenta (uživo) je biblioteka FastComments veština agenta koja pokriva widget, SSO, REST API, moderaciju i migraciju sa Disqus‑a. Remixujte je i agent Val Town‑a, Townie, automatski preuzima veštine iz skills/, tako da vaš agent zna kako da postavi komentare bez da vi lepite dokumentaciju u ćaskanje.

Iste veštine se instaliraju bilo gde drugde pomoću npx skills add fastcomments/skills.