FastComments.com

Live-Kommentare zu Val Town-Apps hinzufügen

Val Town führt TypeScript auf Deno aus, sodass ein Val ein echter Server ist. Das macht es zu einer guten Wahl für FastComments: Das Widget ist ein Script‑Tag auf der Seite, und alles, was ein Geheimnis benötigt, wie Secure SSO oder die Verifizierung eines Webhooks, kann serverseitig im selben Val ausgeführt werden.

Dieser Leitfaden behandelt das Hinzufügen des Kommentar‑Widgets zu einem HTTP‑Val, das Anzeigen von Kommentarzahlen auf einer Indexseite, das Anmelden von Benutzern mit dem bereits vorhandenen Val‑Town‑Konto und das Empfangen von Kommentar‑Webhooks.

Sie benötigen kein Konto, um es auszuprobieren. Die Beispiele verwenden tenantId: "demo", eine gemeinsam genutzte Sandbox, und Schritt 2 behandelt das Wechseln zu Ihrem eigenen.

Kommentaranzahl auf einer Indexseite 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:

Massenkommentarzähler auf einer Indexseite
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.

Sichere SSO mit std/oauth Internal Link

If Ihr Val bereits weiß, wer der Besucher ist, übergibt Secure SSO diese Identität an das Widget, sodass sie nie einen zweiten Login sehen. Es gibt keine Endpunkte zu erstellen und nichts, das zur Laufzeit aufgerufen werden muss: Sie berechnen drei Werte serverseitig und übergeben sie in der Widget‑Konfiguration.

Val Town liefert eine Null‑Konfigurations‑Anmeldung mit std/oauth, sodass der Besucher sich mit dem Val Town‑Konto anmelden kann, das er bereits hat. Ersetzen Sie dies durch das, was Ihre App verwendet; der FastComments‑Teil ändert sich nicht.

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 muss für dieselbe Person stabil sein, sonst erhalten sie bei jedem Login eine neue Kommentaridentität.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // E‑Mail ist erforderlich und muss eindeutig sein.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // Benutzername ist erforderlich und darf keine E‑Mail sein.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() gibt { userDataJSONBase64, verificationHash, timestamp } zurück. Diese drei Werte sind alles, was den Browser erreicht. Das Geheimnis signiert sie und wird dann verworfen, sodass nichts auf der Seite einem Leser ermöglicht, einen anderen Benutzer zu fälschen.

Pass it to the widget

Widget-Konfiguration mit 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 // ...rendern Sie das Widget mit dieser Konfiguration
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware fügt GET /auth/login, GET /auth/callback und POST /auth/logout für Sie hinzu. Beachten Sie, dass Logout ein POST ist, während das Widget zu logoutURL mit einem GET navigiert, also richten Sie logoutURL auf eine kleine eigene Route, die den POST ausführt.

Wenn der Besucher abgemeldet ist, übergeben Sie sso nur mit einer loginURL. Das Widget zeigt dann eine Anmeldeaufforderung anstelle eines anonymen Kommentarfelds.

Things that go wrong

timestamp ist Epoch Millisekunden, darf nicht in der Zukunft liegen und nicht älter als zwei Tage sein. Generieren Sie es auf dem Server in derselben Anfrage, die den Hash berechnet. Die Erzeugung im Browser ist der klassische Fehler: Der Wert unterscheidet sich von dem, der gehasht wurde, und jeder Kommentar wird abgelehnt.

Setzen Sie niemals isAdmin oder isModerator vom Identitätsanbieter. Die Anmeldung mit einem Val Town‑Konto sagt nichts darüber aus, wer Ihre Seite moderieren sollte.

Siehe den SSO guide für die vollständige Feldliste, gruppenbasierte Threads und Badges.

Empfangen von Webhooks Internal Link

Ein val ist ein natürlicher Webhook‑Empfänger: Er hat eine stabile URL, kann eine Signatur verifizieren und hat SQLite‑ und Blob‑Speicher integriert.

FastComments signiert ${timestamp}.${body} mit dem API‑Geheimnis Ihres Kontos und sendet zwei Header:

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

Die Methode überträgt das Ereignis: PUT für einen erstellten oder aktualisierten Kommentar, DELETE für einen gelöschten.

Verifizierung einer Zustellung
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

Zwei Dinge, die Probleme verursachen

Verifiziere die rohen Bytes. Das Parsen des JSON und das erneute Serialisieren ändert die Schlüsselreihenfolge und Leerzeichen, sodass der Hash abweicht und jede Zustellung fehlschlägt, ohne offensichtlichen Grund. Das ist der übliche Grund, warum ein Webhook‑Empfänger „einfach nicht funktioniert“.

Vergleiche in konstanter Zeit. Ein einfaches === auf die Signatur leckt, wie viele Bytes übereinstimmen, was ausreicht, um Byte für Byte zu fälschen.

Ereignisse verarbeiten

Antworte schnell. FastComments versucht es bei einem Nicht‑2xx‑Status erneut, und ein Endpunkt, der ständig fehlschlägt, wird schließlich automatisch deaktiviert. Führe daher die eigentliche Arbeit erst nach der Antwort aus, nicht inline.

Stelle sicher, dass die Verarbeitung anhand der Kommentar‑ID idempotent ist. Ein Retry wird mit einem neuen Zeitstempel erneut signiert, und dieselbe Kommentar‑ID kommt bei Bearbeitung und Löschung erneut, sodass es nichts Stabiles zum Deduplizieren gibt.


Beispielwerte Internal Link

Vier öffentliche Vals, die Sie remixen können, jede deckt einen Teil dieses Leitfadens ab.

Blog mit Kommentaren (live) ist ein Markdown‑Blog mit einem Thread unter jedem Beitrag und einer Gesamtsumme der Kommentare im Index. Er funktioniert sofort, wenn Sie ihn remixen, und eine Umgebungsvariable verweist auf Ihr eigenes Konto.

SSO‑Demo (live) meldet den Besucher mit seinem Val‑Town‑Konto an und übergibt diese Identität an das Widget, sodass kein zweiter Login nötig ist.

Webhook‑Empfänger (live) prüft die HMAC‑Signatur bei jeder Zustellung und speichert Ereignisse in SQLite. Er hat einen Button, der eine Test‑Payload signiert und an sich selbst liefert, sodass Sie die erfolgreiche Verifizierung beobachten können, bevor Sie einen echten Webhook konfigurieren.

Agent‑Skills (live) ist eine Bibliothek von FastComments‑Agent‑Skills, die das Widget, SSO, die REST‑API, Moderation und die Migration von Disqus abdecken. Remixen Sie sie und der Agent von Val Town, Townie, übernimmt die Skills automatisch aus skills/, sodass Ihr Agent weiß, wie Kommentare eingerichtet werden, ohne dass Sie Dokumentation in den Chat einfügen müssen.

Die gleichen Skills können überall sonst mit npx skills add fastcomments/skills installiert werden.