FastComments.com

Dodaj komentarze na żywo do aplikacji Val Town


Val Town uruchamia TypeScript na Deno, więc val jest prawdziwym serwerem. To sprawia, że jest to dobre dopasowanie do FastComments: widget jest tagiem skryptu na stronie, a wszystko, co wymaga sekretu, jak Secure SSO czy weryfikacja webhooka, może działać po stronie serwera w tym samym val.

Ten przewodnik opisuje dodawanie widgetu komentarzy do HTTP val, wyświetlanie liczby komentarzy na stronie indeksu, logowanie użytkowników przy użyciu konta Val Town, które już posiadają, oraz odbieranie webhooków komentarzy.

Nie potrzebujesz konta, aby to wypróbować. Przykłady używają tenantId: "demo", współdzielonego sandboxu, a krok 2 opisuje przejście na własny.


Liczba komentarzy na stronie indeksu Internal Link

Na stronie indeksu nie renderuj jednego widżetu liczenia komentarzy na wiersz. To powoduje jedno żądanie na każdy post. Użyj liczenia zbiorczego, które wymaga jednego żądania dla całej strony.

Oznacz każdy wiersz urlId, którego używa wątek, a następnie załaduj widżet zbiorczy jednorazowo:

Zbiorcze liczenie komentarzy na indeksie
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

Skrypt znajduje każdy element .fast-comments-count na stronie i wstawia jego liczbę.

data-fast-comments-url-id musi odpowiadać urlId, którego używa widżet komentarzy posta. Jeśli widżet używa slug, znacznik używa slug. Niezgodność powoduje wyświetlenie zera w wątku, który ma komentarze.

Skrypt nasłuchuje window.FastCommentsBulkCountConfig, więc nie ma znaczenia, czy ustawisz konfigurację przed, czy po znaczniku skryptu.

Bezpieczne SSO z 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 musi być stabilny dla tej samej osoby, w przeciwnym razie otrzyma nową tożsamość komentarza przy każdym logowaniu.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email jest wymagany i musi być unikalny.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // nazwa użytkownika jest wymagana i nie może być adresem 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

Konfiguracja widgetu z 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 // ...renderuj widget z tą konfiguracją
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.

Odbieranie webhooków Internal Link

A val jest naturalnym odbiorcą webhooków: ma stabilny URL, może weryfikować podpis i ma wbudowaną bazę SQLite oraz przechowywanie blobów.

FastComments podpisuje ${timestamp}.${body} przy użyciu sekretu API Twojego konta i wysyła dwa nagłówki:

Nagłówki webhooka
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

Metoda przenosi zdarzenie: PUT dla utworzonego lub zaktualizowanego komentarza, DELETE dla usuniętego.

Weryfikacja dostawy
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Dokładne bajty, które nadeszły. Nie używaj c.req.json() i nie serializuj ponownie.
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 // Odrzuć przestarzałe dostawy, aby przechwycony request nie mógł być odtworzony później.
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

Dwie rzeczy, które gryzą

Zweryfikuj surowe bajty. Parsowanie JSON i ponowne serializowanie zmienia kolejność kluczy oraz białe znaki, więc hash się różni i każda dostawa kończy się niepowodzeniem bez oczywistej przyczyny. To najczęstszy powód, dla którego odbiorca webhooka „po prostu nie działa”.

Porównuj w stałym czasie. Zwykłe === na podpisie ujawnia, ile bajtów się zgadza, co wystarczy, aby podrobić jeden bajt naraz.

Obsługa zdarzeń

Odpowiadaj szybko. FastComments ponawia próbę przy kodzie innym niż 2xx, a endpoint, który ciągle zawodzi, zostaje ostatecznie automatycznie wyłączony, więc wykonuj rzeczywistą pracę po odpowiedzi, a nie w linii.

Uczyń to działanie idempotentnym względem identyfikatora komentarza. Ponowna próba jest podpisywana ponownie z nowym znacznikiem czasu, a ten sam identyfikator komentarza pojawia się ponownie przy edycji i usunięciu, więc nie ma nic stabilnego, na czym można by deduplikować.


Przykładowe Vals Internal Link

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

Blog with comments (live) jest blogiem w formacie Markdown z wątkiem pod każdym postem i zbiorczymi licznikami komentarzy na indeksie. Działa od razu po remixowaniu, a jedna zmienna środowiskowa wskazuje na twoje własne konto.

SSO demo (live) loguje odwiedzającego przy użyciu jego konta Val Town i przekazuje tę tożsamość do widgetu, więc nie ma drugiego logowania.

Webhook receiver (live) weryfikuje podpis HMAC przy każdej dostawie i przechowuje zdarzenia w SQLite. Ma przycisk, który podpisuje testowy payload i dostarcza go do siebie, więc możesz obserwować pomyślną weryfikację przed skonfigurowaniem prawdziwego webhooka.

Agent skills (live) jest biblioteką umiejętności agenta FastComments obejmującą widget, SSO, REST API, moderację i migrację z Disqus. Remixuj ją, a agent Val Town, Townie, automatycznie pobiera umiejętności z skills/, więc twój agent wie, jak podłączyć komentarze bez wklejania dokumentacji do czatu.

The same skills install anywhere else with npx skills add fastcomments/skills.