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

Rendern Sie auf einer Indexseite nicht ein Kommentarzahl-Widget pro Zeile. Das wäre eine Anfrage pro Beitrag. Verwenden Sie die Bulk-Zählung, die eine einzige Anfrage für die gesamte Seite benötigt.

Markieren Sie jede Zeile mit der urlId, die ihr Thread verwendet, und laden Sie dann das Bulk-Widget einmal:

Bulk-Kommentarzahlen 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

Das Skript findet jedes .fast-comments-count-Element auf der Seite und füllt dessen Anzahl aus.

data-fast-comments-url-id muss mit der urlId übereinstimmen, die das Kommentar-Widget dieses Beitrags verwendet. Wenn das Widget den Slug verwendet, verwendet der Marker den Slug. Eine Nichtübereinstimmung zeigt null bei einem Thread an, der Kommentare hat.

Das Skript fragt window.FastCommentsBulkCountConfig ab, daher spielt es keine Rolle, ob Sie die Konfiguration vor oder nach dem Skript-Tag setzen.

Sichere SSO mit std/oauth Internal Link

Wenn dein Val bereits weiß, wer der Besucher ist, übergibt Secure SSO diese Identität an das Widget, sodass der Nutzer nie eine zweite Anmeldung sieht. Es gibt keine Endpunkte zu bauen und nichts zur Laufzeit aufzurufen: Du berechnest drei Werte serverseitig und übergibst sie in der Widget-Konfiguration.

Val Town bietet Login ohne Konfiguration mit std/oauth, sodass sich der Besucher mit dem Val Town-Konto anmelden kann, das er bereits hat. Tausche das gegen alles aus, was deine App verwendet; der FastComments-Teil ändert sich nicht.

Payload auf dem Server erstellen

Der API-Schlüssel signiert den Payload und darf niemals Browser-Code erreichen. Installiere das SDK von npm, das auf Val Towns Deno-Runtime unverändert funktioniert:

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() gibt { userDataJSONBase64, verificationHash, timestamp } zurück. Diese drei Werte sind alles, was den Browser erreicht. Der Schlüssel signiert sie und wird dann verworfen, sodass nichts auf der Seite einem Leser ermöglicht, einen anderen Benutzer zu fälschen.

An das Widget übergeben

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 // ...render the widget with this config
17});
18
19export default oauthMiddleware(app.fetch);
20

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

Wenn der Besucher ausgeloggt ist, übergib sso nur mit einer loginURL. Das Widget zeigt dann eine Login-Aufforderung anstelle eines anonymen Kommentarfelds.

Häufige Probleme

timestamp ist in Epoch-Millisekunden, darf nicht in der Zukunft liegen und darf nicht älter als zwei Tage sein. Generiere es auf dem Server in derselben Anfrage, die den Hash berechnet. Die Generierung im Browser ist der klassische Fehler: Der Wert unterscheidet sich vom gehashten Wert und jeder Kommentar wird abgelehnt.

Setze niemals isAdmin oder isModerator vom Identity-Provider. Die Anmeldung mit einem Val Town-Konto sagt nichts darüber aus, wer deine Seite moderieren sollte.

Siehe den SSO-Leitfaden für die vollständige Feldliste, gruppenbeschränkte Threads und Abzeichen.

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.