FastComments.com

Tilføj live-kommentarer til Val Town-apps

Val Town kører TypeScript på Deno, så en val er en rigtig server. Det gør den velegnet til FastComments: widget'en er et script‑tag på siden, og alt der har brug for en hemmelighed, som Secure SSO eller verifikation af en webhook, kan køre server‑side i den samme val.

Denne guide dækker, hvordan du tilføjer kommentarswidget'en til en HTTP‑val, viser kommentarantal på en indeks‑side, logger brugere ind med den Val Town‑konto, de allerede har, og modtager kommentar‑webhooks.

Du behøver ikke en konto for at prøve det. Eksemplerne bruger tenantId: "demo", en delt sandbox, og trin 2 dækker, hvordan du skifter til din egen.

Kommentarantal på en indeks-side 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:

Bulk-kommentarantal på en indeks
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.

Sikre SSO med 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

Widget-konfiguration med 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.

Modtagelse af webhooks Internal Link

En val er en naturlig webhook-modtager: den har en stabil URL, den kan verificere en signatur, og den har SQLite og blob-lagring indbygget.

FastComments signerer ${timestamp}.${body} med din kontos API-hemmelighed og sender to headers:

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

Metoden bærer hændelsen: PUT for en oprettet eller opdateret kommentar, DELETE for en slettet.

Verificering af en levering
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

To ting der bider

Verificer de rå bytes. Parsing af JSON og gen-serialisering ændrer nøgleordren og mellemrum, så hash'en er forskellig, og hver levering fejler uden en åbenlys årsag. Dette er den sædvanlige grund til, at en webhook-modtager "bare ikke virker".

Sammenlign i konstant tid. En simpel === på signaturen lækker hvor mange bytes der matchede, hvilket er nok til at forfalske en byte ad gangen.

Håndtering af hændelser

Svar hurtigt. FastComments forsøger igen ved et svar, der ikke er 2xx, og et endpoint, der fortsat fejler, deaktiveres til sidst automatisk, så udfør reelt arbejde efter at have svaret i stedet for inline.

Gør arbejdet idempotent på kommentar-id'et. Et retry bliver gen-signeret med en frisk tidsstempel, og det samme kommentar-id ankommer igen ved redigering og sletning, så der er intet stabilt at deduplere på.


Eksempel Vals Internal Link

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

Blog med kommentarer (live) er en Markdown-blog med en tråd under hvert indlæg og samlede kommentarantal på indeks-siden. Den fungerer i det øjeblik, du remix'er den, og én miljøvariabel peger den på din egen konto.

SSO-demo (live) logger besøgende ind med deres Val Town-konto og overlever den identitet til widget'en, så der ikke er noget andet login.

Webhook-modtager (live) verificerer HMAC-signaturen på hver levering og gemmer hændelser i SQLite. Den har en knap, der underskriver en testpayload og leverer den til sig selv, så du kan se verifikationen lykkes, før du konfigurerer en rigtig webhook.

Agent færdigheder (live) er et bibliotek af FastComments-agentfærdigheder, der dækker widget'en, SSO, REST API'et, moderation og migrering væk fra Disqus. Remix den, og Val Town's agent, Townie, henter færdighederne fra skills/ automatisk, så din agent ved, hvordan man integrerer kommentarer uden at du skal indsætte dokumentation i chatten.

De samme færdigheder kan installeres hvor som helst andet med npx skills add fastcomments/skills.