FastComments.com

Προσθήκη ζωντανής σχολιασμού στις εφαρμογές Val Town

Val Town εκτελεί TypeScript στο Deno, έτσι ένα val είναι ένας πραγματικός διακομιστής. Αυτό το καθιστά κατάλληλο για το FastComments: το widget είναι μια ετικέτα script στη σελίδα, και οτιδήποτε χρειάζεται ένα μυστικό, όπως Secure SSO ή η επαλήθευση ενός webhook, μπορεί να εκτελεστεί στο διακομιστή στο ίδιο val.

Αυτός ο οδηγός καλύπτει την προσθήκη του widget σχολίων σε ένα HTTP val, την εμφάνιση του αριθμού σχολίων σε μια σελίδα ευρετηρίου, τη σύνδεση χρηστών με τον λογαριασμό Val Town που ήδη έχουν, και τη λήψη webhook σχολίων.

Δεν χρειάζεστε λογαριασμό για να το δοκιμάσετε. Τα παραδείγματα χρησιμοποιούν tenantId: "demo", ένα κοινόχρηστο sandbox, και το Βήμα 2 καλύπτει τη μετάβαση στο δικό σας.

Αριθμοί σχολίων σε σελίδα ευρετηρίου Internal Link


Σε μια σελίδα ευρετηρίου, μην αποδίδετε ένα widget καταμέτρησης σχολίων ανά γραμμή. Αυτό σημαίνει ένα αίτημα ανά ανάρτηση. Χρησιμοποιήστε τη μαζική μέτρηση, η οποία απαιτεί ένα μόνο αίτημα για ολόκληρη τη σελίδα.

Σημειώστε κάθε γραμμή με το urlId που χρησιμοποιεί το νήμα της, και στη συνέχεια φορτώστε το μαζικό widget μία φορά:

Μαζικές μετρήσεις σχολίων σε ευρετήριο
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

Το script εντοπίζει κάθε στοιχείο .fast-comments-count στη σελίδα και γεμίζει τη μέτρησή του.

data-fast-comments-url-id πρέπει να ταιριάζει με το urlId που χρησιμοποιεί το widget σχολίων της ανάρτησης. Εάν το widget χρησιμοποιεί το slug, ο δείκτης χρησιμοποιεί το slug. Μια ασυμφωνία εμφανίζει μηδέν σε ένα νήμα που έχει σχόλια.

Το script ελέγχει περιοδικά το window.FastCommentsBulkCountConfig, επομένως δεν έχει σημασία αν ορίσετε τη διαμόρφωση πριν ή μετά το στοιχείο script.

Ασφαλής SSO με 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 πρέπει να είναι σταθερό για το ίδιο άτομο, αλλιώς θα λαμβάνει νέα ταυτότητα σχολίου σε κάθε σύνδεση.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // Το email είναι υποχρεωτικό και πρέπει να είναι μοναδικό.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // Το όνομα χρήστη είναι υποχρεωτικό και δεν μπορεί να είναι 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 με 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 // ...απόδοση του widget με αυτή τη διαμόρφωση
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 for the full field list, group-gated threads, and badges.

Λήψη Webhooks Internal Link

A val είναι ένας φυσικός δέκτης webhook: διαθέτει σταθερό URL, μπορεί να επαληθεύσει μια υπογραφή και έχει ενσωματωμένη αποθήκευση SQLite και blob.

FastComments υπογράφει ${timestamp}.${body} με το μυστικό API του λογαριασμού σας και στέλνει δύο κεφαλίδες:

Κεφαλίδες Webhook
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

Η μέθοδος μεταφέρει το γεγονός: PUT για ένα δημιουργημένο ή ενημερωμένο σχόλιο, DELETE για ένα διαγραμμένο.

Επαλήθευση παράδοσης
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

Δύο πράγματα που προκαλούν προβλήματα

Επαληθεύστε τα ακατέργαστα byte. Η ανάλυση του JSON και η επανα-σειριοποίησή του αλλάζει τη σειρά των κλειδιών και τα κενά, έτσι το hash διαφέρει και κάθε παράδοση αποτυγχάνει χωρίς προφανή λόγο. Αυτό είναι ο συνηθισμένος λόγος που ένας δέκτης webhook «απλώς δεν λειτουργεί».

Συγκρίνετε σε σταθερό χρόνο. Ένα απλό === στην υπογραφή διαρρέει πόσα byte ταιριάζουν, κάτι που αρκεί για να παραχθεί ένα byte τη φορά.

Διαχείριση συμβάντων

Απαντήστε γρήγορα. Το FastComments κάνει επανεγγραφές σε μη-2xx απαντήσεις, και ένα endpoint που συνεχίζει να αποτυγχάνει απενεργοποιείται αυτόματα μετά από κάποιο χρόνο, επομένως εκτελέστε την πραγματική εργασία μετά την απάντηση αντί εντός της απάντησης.

Κάντε αυτή τη λειτουργία αμετάβλητη (idempotent) με βάση το αναγνωριστικό του σχολίου. Μια επανεγγραφή υπογράφεται ξανά με νέο χρονικό σήμα, και το ίδιο αναγνωριστικό σχολίου φτάνει ξανά σε επεξεργασία και διαγραφή, οπότε δεν υπάρχει κάτι σταθερό για απο-διπλοεγγραφή.

Παραδείγματα Vals Internal Link

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

Blog with comments (live) είναι ένα blog σε Markdown με νήμα κάτω από κάθε ανάρτηση και συνολικές μετρήσεις σχολίων στον δείκτη. Λειτουργεί τη στιγμή που το επαναχρησιμοποιείτε, και μια μεταβλητή περιβάλλοντος το κατευθύνει στον δικό σας λογαριασμό.

SSO demo (live) συνδέει τον επισκέπτη με τον λογαριασμό του Val Town και παραδίδει αυτή την ταυτότητα στο widget, ώστε να μην υπάρχει δεύτερη σύνδεση.

Webhook receiver (live) επαληθεύει την υπογραφή HMAC σε κάθε παράδοση και αποθηκεύει τα γεγονότα σε SQLite. Διαθέτει ένα κουμπί που υπογράφει ένα δοκιμαστικό payload και το αποστέλλει στον εαυτό του, ώστε να μπορείτε να δείτε την επαλήθευση να πετυχαίνει πριν ρυθμίσετε ένα πραγματικό webhook.

Agent skills (live) είναι μια βιβλιοθήκη δεξιοτήτων πράκτορα FastComments που καλύπτει το widget, το SSO, το REST API, τη διαχείριση και τη μετάβαση από το Disqus. Επαναχρησιμοποιήστε το και ο πράκτορας της Val Town, Townie, αντλεί αυτόματα τις δεξιότητες από το skills/, ώστε ο πράκτοράς σας να ξέρει πώς να ενσωματώνει σχόλια χωρίς να επικολλάτε τεκμηρίωση στη συνομιλία.

Οι ίδιες δεξιότητες εγκαθίστανται οπουδήποτε αλλού με npx skills add fastcomments/skills.