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

Als je val al weet wie de bezoeker is, geeft Secure SSO die identiteit door aan de widget zodat deze nooit een tweede login te zien krijgt. Er zijn geen endpoints om te bouwen en niets om tijdens runtime aan te roepen: je berekent drie waarden server-side en geeft ze door in de widget-config.

Val Town levert zero-config login mee met std/oauth, zodat de bezoeker kan inloggen met het Val Town-account dat hij al heeft. Vervang dat door wat jouw app gebruikt; het FastComments-gedeelte verandert niet.

Bouw de payload op de server

De API secret ondertekent de payload en mag nooit in browsercode terechtkomen. Installeer de SDK vanaf npm, die op Val Town's Deno-runtime direct werkt:

sso.ts
Copy CopyRun External Link
1
2import { SecureSSOPayloadBuilder } from "npm:fastcomments-sdk/server";
3
4export function buildSSOPayload(user) {
5 // id moet stabiel zijn voor dezelfde persoon, anders krijgt die bij elke login een nieuwe comment-identiteit.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email is verplicht en moet uniek zijn.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // username is verplicht en mag geen e-mailadres zijn.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() geeft { userDataJSONBase64, verificationHash, timestamp } terug. Die drie waarden zijn alles wat de browser bereikt. De secret ondertekent ze en wordt daarna weggegooid, dus niets op de pagina stelt een lezer in staat om een andere gebruiker te vervalsen.

Geef het door aan de widget

Widget config 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 de widget met deze config
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware voegt GET /auth/login, GET /auth/callback en POST /auth/logout voor je toe. Let op: logout is een POST, terwijl de widget naar logoutURL navigeert met een GET, dus laat logoutURL wijzen naar een eigen kleine route die de POST verstuurt.

Wanneer de bezoeker is uitgelogd, geef je sso door met alleen een loginURL. De widget toont dan een login-prompt in plaats van een anoniem commentaarvak.

Dingen die misgaan

timestamp is epoch in milliseconden, mag niet in de toekomst liggen en mag niet ouder zijn dan twee dagen. Genereer het op de server in dezelfde request waarin de hash wordt berekend. Het in de browser genereren is de klassieke fout: de waarde verschilt van degene die gehasht is en elk commentaar wordt geweigerd.

Zet isAdmin of isModerator nooit op basis van de identity provider. Inloggen met een Val Town-account zegt niets over wie jouw site moet modereren.

Zie de SSO-gids voor de volledige lijst met velden, threads met groepstoegang en 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

Vier openbare vals die je kunt remixen, die elk één onderdeel van deze gids behandelen.

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

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

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

Agent skills (live) is een bibliotheek met FastComments agent skills die de widget, SSO, de REST API, moderatie en migreren van Disqus beslaan. Remix het en Val Town's agent, Townie, pakt de skills automatisch op uit skills/, zodat je agent weet hoe hij reacties moet opzetten zonder dat je documentatie in de chat plakt.

Dezelfde skills installeren overal elders met npx skills add fastcomments/skills.