FastComments.com

Val Town Uygulamalarına Canlı Yorum Ekleme

Val Town Deno üzerinde TypeScript çalıştırır, bu yüzden bir val gerçek bir sunucudur. Bu, FastComments için iyi bir uyum sağlar: widget sayfada bir script etiketi olarak bulunur ve Secure SSO gibi bir gizli bilgiye ya da bir webhook doğrulamaya ihtiyaç duyan her şey aynı val içinde sunucu tarafında çalıştırılabilir.

Bu kılavuz, yorum widget'ını bir HTTP val'ına eklemeyi, bir indeks sayfasında yorum sayılarını göstermeyi, kullanıcıları zaten sahip oldukları Val Town hesabı ile oturum açtırmayı ve yorum webhook'larını almayı kapsar.

Bunu denemek için bir hesaba ihtiyacınız yok. Örnekler tenantId: "demo" adlı paylaşılan bir sandbox kullanır ve Adım 2, kendi sandbox'ınıza geçişi kapsar.

İndeks Sayfasındaki Yorum Sayıları 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:

Dizin üzerindeki toplu yorum sayıları
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.

std/oauth ile Güvenli SSO 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 aynı kişi için sabit olmalı, aksi takdirde her oturum açmada yeni bir yorum kimliği alırlar.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email gereklidir ve benzersiz olmalıdır.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // kullanıcı adı gereklidir ve bir email olamaz.
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

SSO ile Widget yapılandırması
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 // ...bu yapılandırma ile widget'ı render edin
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.

Webhook'ları Alma Internal Link

A val, doğal bir webhook alıcısıdır: sabit bir URL'ye sahiptir, bir imzayı doğrulayabilir ve SQLite ile blob depolama yerleşiktir.

FastComments, ${timestamp}.${body} ifadesini hesabınızın API gizli anahtarıyla imzalar ve iki başlık gönderir:

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

Yöntem olayı taşır: oluşturulan veya güncellenen bir yorum için PUT, silinen bir yorum için DELETE.

Teslimatı doğrulama
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

Sorun yaratan iki şey

Ham baytları doğrulayın. JSON'u ayrıştırıp yeniden serileştirmek, anahtar sırasını ve boşlukları değiştirir, bu yüzden hash farklı olur ve her teslimat belirgin bir neden olmadan başarısız olur. Bu, bir webhook alıcısının “sadece çalışmıyor” olmasının yaygın nedenidir.

Sabit zamanlı karşılaştırma yapın. İmza üzerinde basit bir === ifadesi, kaç baytın eşleştiğini sızdırır ve bu, tek tek bir baytı taklit etmek için yeterlidir.

Olayları işleme

Hızlı yanıt verin. FastComments, 2xx olmayan bir yanıt alındığında yeniden deneme yapar ve sürekli başarısız olan bir uç nokta sonunda otomatik olarak devre dışı bırakılır, bu yüzden gerçek işi yanıt verdikten sonra yapın, satır içinde değil.

Bu işlemi yorum kimliği üzerinde idempotent (tekrarlanabilir) hâle getirin. Bir yeniden deneme, yeni bir zaman damgası ile yeniden imzalanır ve aynı yorum kimliği düzenleme ve silme sırasında tekrar gelir, bu yüzden tutarlı bir şekilde yinelenenleri ayıklamak için sabit bir şey yoktur.

Örnek Vals Internal Link


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

Blog with comments (live) bir Markdown blogudur, her gönderinin altında bir konu başlığı ve indeksde toplu yorum sayıları bulunur. Karıştırdığınız anda çalışır ve bir ortam değişkeni onu kendi hesabınıza yönlendirir.

SSO demo (live) ziyaretçiyi Val Town hesabıyla oturum açtırır ve bu kimliği widget'a verir, böylece ikinci bir oturum açma gerekmez.

Webhook receiver (live) her teslimatta HMAC imzasını doğrular ve olayları SQLite'da saklar. Test bir yük imzalayan ve kendisine teslim eden bir düğmesi vardır, böylece gerçek bir webhook yapılandırmadan önce doğrulamanın başarılı olduğunu izleyebilirsiniz.

Agent skills (live) FastComments ajan becerilerini içeren bir kütüphanedir; widget, SSO, REST API, moderasyon ve Disqus'tan geçişi kapsar. Bunu karıştırdığınızda Val Town'un ajanı Townie, skills/ içindeki becerileri otomatik olarak alır, böylece ajanınız sohbet içine belge yapıştırmadan yorumları nasıl bağlayacağını bilir.

Aynı beceriler, npx skills add fastcomments/skills komutuyla başka bir yerde de kurulabilir.