FastComments.com

Добавете живо коментиране към приложенията на Val Town

Val Town изпълнява TypeScript върху Deno, така че val е истински сървър. Това го прави подходящ за FastComments: уиджетът е script таг в страницата и всичко, което изисква тайна, като Secure SSO или проверка на webhook, може да се изпълни от сървърната страна в същия val.

Този наръчник обхваща добавянето на уиджета за коментари към 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

Скриптът намира всеки елемент .fast-comments-count на страницата и попълва неговия брой.

data-fast-comments-url-id трябва да съвпада с urlId, който използва widget‑ът за коментари на публикацията. Ако widget‑ът използва slug, маркерът използва slug. При несъответствие се показва нула за нишка, която има коментари.

Скриптът проверява наличието на window.FastCommentsBulkCountConfig, затова няма значение дали конфигурацията е зададена преди или след етикета на скрипта.

Сигурен SSO с std/oauth Internal Link

Ако вашият val вече знае кой е посетителят, Secure SSO предава тази идентичност на уиджета, така че той никога да не вижда второ влизане. Няма нужда от създаване на крайни точки и нищо не се извиква по време на изпълнение: изчислявате три стойности на сървъра и ги предавате в конфигурацията на уиджета.

Val Town предоставя вход без конфигурация с std/oauth, така че посетителят може да влезе с Val Town акаунта, който вече притежава. Заменете това с каквото вашето приложение използва; частта на FastComments остава непроменена.

Създаване на полезния товар на сървъра

Тайната на API подписва полезния товар и никога не трябва да достигне до кода в браузъра. Инсталирайте SDK от npm, което работи директно в Deno средата на Val Town:

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() връща { userDataJSONBase64, verificationHash, timestamp }. Тези три стойности са всичко, което достига до браузъра. Тайната ги подписва и след това се изхвърля, така че нищо в страницата не позволява на читателя да подправи различен потребител.

Предайте го на уиджета

Конфигурация на уиджет с 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 добавя GET /auth/login, GET /auth/callback и POST /auth/logout за вас. Обърнете внимание, че изходът е POST, докато уиджетът навигира към logoutURL с GET, затова насочете logoutURL към малка ваша рута, която изпраща POST заявка.

Когато посетителят е излязъл, предайте sso само с loginURL. Уиджетът тогава показва прозорец за вход вместо анонимен кутия за коментари.

Неочаквани проблеми

timestamp е епоха в милисекунди, не трябва да бъде в бъдещето и не трябва да е по-стар от два дни. Генерирайте го на сървъра в същата заявка, която изчислява хеша. Генерирането му в браузъра е класическа грешка: стойността се различава от тази, която е била хеширана, и всеки коментар се отхвърля.

Никога не задавайте isAdmin или isModerator от доставчика на идентичност. Влизането с Val Town акаунт не казва нищо за това кой трябва да модерира вашия сайт.

Вижте ръководството за SSO за пълен списък на полетата, нишки с групово ограничение и значки.

Получаване на уебкуки Internal Link

A val е естествен получател на уебхук: има стабилен URL, може да проверява подпис и има вградено SQLite и съхранение на блобове.

FastComments подписва ${timestamp}.${body} с API тайната на вашия акаунт и изпраща две заглавки:

Заглавки на уебхук
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 // Точните байтове, които пристигнаха. НЕ използвайте c.req.json() и повторно сериализиране.
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 // Отхвърляне на остарели доставки, за да не може прихванатата заявка да бъде повторно изпълнена по-късно.
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 // ...обработете JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Две неща, които късат

Проверете суровите байтове. Парсирането на JSON и повторното му сериализиране променя реда на ключовете и whitespace, така че хешът се различава и всяка доставка се проваля без очевидна причина. Това е обичайната причина уебхук получателят „просто не работи“.

Сравнявайте в константно време. Обикновен === върху подписа разкрива колко байта съвпадат, което е достатъчно за фалшифициране по един байт наведнъж.

Обработка на събития

Отговаряйте бързо. FastComments прави повторни опити при не‑2xx отговор, а крайна точка, която продължава да се проваля, в крайна сметка се изключва автоматично, затова извършвайте реалната работа след отговора, а не вмъкната.

Направете тази работа идемпотентна спрямо ID‑то на коментара. При повторен опит подписът се генерира отново с нов timestamp, а същото ID на коментара се получава отново при редактиране и изтриване, така че няма стабилна стойност за дедупликация.

Примерни стойности Internal Link

Четири публични vals, които можете да ремиксирате, всяка от тях обхваща една част от това ръководство.

Blog with comments (live) е Markdown блог с нишка под всяка публикация и общи броячи на коментари в индекса. Работи веднага след като го ремиксирате, а една променлива на средата го насочва към вашия собствен акаунт.

SSO demo (live) вписва посетителя с техния Val Town акаунт и предава тази идентичност на уиджета, така че няма второ влизане.

Webhook receiver (live) проверява HMAC подписа при всяка доставка и съхранява събития в SQLite. Има бутон, който подписва тестово натоварване и го изпраща към себе си, така че можете да видите проверката да успее преди да конфигурирате истински webhook.

Agent skills (live) е библиотека от FastComments агентски умения, обхващащи уиджета, SSO, REST API, модериране и миграция от Disqus. Ремиксирайте я и агентът на Val Town, Townie, автоматично взема уменията от skills/, така че вашият агент знае как да настрои коментарите без да копирате документацията в чата.

Същите умения се инсталират навсякъде другаде с npx skills add fastcomments/skills.