FastComments.com

Live commentaar toevoegen aan Val Town-apps


Val Town draait TypeScript op Deno, dus een val is een echte server. Dat maakt het een goede match voor FastComments: de widget is een script‑tag op de pagina, en alles wat een geheim nodig heeft, zoals Secure SSO of het verifiëren van een webhook, kan server‑side draaien in dezelfde val.

Deze gids behandelt het toevoegen van de commentaarwidget aan een HTTP val, het tonen van commentaartellingen op een indexpagina, het aanmelden van gebruikers met het Val Town‑account dat ze al hebben, en het ontvangen van commentaar‑webhooks.

Je hebt geen account nodig om het te proberen. De voorbeelden gebruiken tenantId: "demo", een gedeelde sandbox, en Stap 2 behandelt het overschakelen naar je eigen.

Reactietellingen op een indexpagina Internal Link

Op een indexpagina moet je niet per rij één comment-count widget renderen. Dat is één verzoek per bericht. Gebruik de bulk‑telling, die één enkel verzoek voor de hele pagina doet.

Markeer elke rij met de urlId die de thread gebruikt, en laad vervolgens de bulk‑widget één keer:

Bulk commentaartellingen op een index
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

Het script vindt elk .fast-comments-count‑element op de pagina en vult de telling in.

data-fast-comments-url-id moet overeenkomen met de urlId die de commentaarwidget van het bericht gebruikt. Als de widget de slug gebruikt, gebruikt de marker de slug. Een mismatch toont nul op een thread die wel reacties heeft.

Het script pollt naar window.FastCommentsBulkCountConfig, dus het maakt niet uit of je de configuratie vóór of na de script‑tag instelt.

Beveiligde SSO met 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

Widgetconfiguratie met 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.

Webhooks ontvangen Internal Link

A val is een natuurlijke webhook‑ontvanger: het heeft een stabiele URL, het kan een handtekening verifiëren, en het heeft SQLite en blob‑opslag ingebouwd.

FastComments ondertekent ${timestamp}.${body} met het API‑geheim van uw account en stuurt twee headers:

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

De methode draagt het evenement: PUT voor een aangemaakt of bijgewerkt commentaar, DELETE voor een verwijderd commentaar.

Levering verifiëren
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

Twee dingen die bijten

Verifieer de ruwe bytes. Het parseren van de JSON en opnieuw serialiseren verandert de sleutelvolgorde en witruimte, waardoor de hash verschilt en elke levering faalt zonder duidelijke oorzaak. Dit is de gebruikelijke reden waarom een webhook‑ontvanger “gewoon niet werkt”.

Vergelijk in constante tijd. Een eenvoudige === op de handtekening lekt hoeveel bytes overeenkomen, wat genoeg is om één byte per keer te vervalsen.

Gebeurtenissen afhandelen

Antwoord snel. FastComments probeert opnieuw bij een non-2xx, en een endpoint die blijft falen wordt uiteindelijk automatisch uitgeschakeld, dus voer het echte werk uit na het beantwoorden in plaats van inline.

Maak dat werk idempotent op basis van de commentaar‑id. Een retry wordt opnieuw ondertekend met een verse timestamp, en dezelfde commentaar‑id komt opnieuw terug bij bewerken en verwijderen, dus er is niets stabiels om te dedupliceren op.


Voorbeeld Vals Internal Link

Four public vals you can remix, each covering one piece of this guide.

Blog met reacties (live) is een Markdown-blog met een thread onder elk bericht en bulkreactietellingen op de index. Het werkt meteen nadat je het remixt, en één omgevingsvariabele wijst het naar je eigen account.

SSO-demo (live) meldt de bezoeker aan met hun Val Town-account en geeft die identiteit door aan de widget, zodat er geen tweede login nodig is.

Webhook-ontvanger (live) verifieert de HMAC-handtekening bij elke levering en slaat gebeurtenissen op in SQLite. Het heeft een knop die een testpayload ondertekent en naar zichzelf verzendt, zodat je de verificatie kunt zien slagen voordat je een echte webhook configureert.

Agent-vaardigheden (live) is een bibliotheek van FastComments-agentvaardigheden die de widget, SSO, de REST API, moderatie en het migreren van Disqus behandelen. Remix het en de agent van Val Town, Townie, haalt de vaardigheden automatisch uit skills/, zodat je agent weet hoe commentaren moeten worden opgezet zonder dat je documentatie in de chat plakt.

Dezelfde vaardigheden kun je overal anders installeren met npx skills add fastcomments/skills.