FastComments.com

Додати живе коментування до додатків Val Town

Val Town працює з TypeScript на Deno, тому val — це справжній сервер. Це робить його хорошим варіантом для FastComments: віджет — це тег script на сторінці, і все, що потребує секрету, наприклад Secure SSO або перевірка вебхука, може виконуватись на сервері в тому ж val.

У цьому посібнику розглядається додавання віджета коментарів до HTTP val, відображення кількості коментарів на головній сторінці, входження користувачів за допомогою облікового запису Val Town, який у них вже є, та отримання вебхуків коментарів.

Для спроби обліковий запис не потрібен. У прикладах використовується tenantId: "demo", спільна пісочниця, а крок 2 охоплює перехід на ваш власний.

Кількість коментарів на індексній сторінці Internal Link

На індексній сторінці не рендерьте один віджет підрахунку коментарів на рядок. Це означає один запит на пост. Використовуйте масовий підрахунок, який робить один запит для всієї сторінки.

Позначте кожен рядок urlId, який використовує його тема, потім завантажте масовий віджет один раз:

Масові підрахунки коментарів на індексній сторінці
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, який використовує віджет коментарів поста. Якщо віджет використовує slug, маркер використовує slug. Невідповідність показує нуль у темі, яка має коментарі.

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

Захищений SSO з std/oauth Internal Link

Якщо ваш val уже знає, хто такий відвідувач, Secure SSO передає цю ідентичність віджету, тож користувач ніколи не побачить повторний вхід. Не потрібно створювати жодних ендпоінтів і нічого викликати під час виконання: ви обчислюєте три значення на сервері та передаєте їх у конфігурацію віджета.

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

Формування payload на сервері

API secret підписує payload і ніколи не повинен потрапляти в код браузера. Встановіть 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 }. Саме ці три значення потрапляють у браузер. Секрет підписує їх, а потім відкидається, тож нічого на сторінці не дозволить зловмиснику підробити іншого користувача.

Передача у віджет

Widget config with 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 за вас. Зверніть увагу, що logout — це POST, тоді як віджет переходить на logoutURL через GET, тому вкажіть logoutURL на невеликий власний маршрут, який надсилає цей POST.

Коли відвідувач не авторизований, передайте sso лише з loginURL. Тоді віджет покаже запрошення до входу замість анонімного поля коментарів.

Типові проблеми

timestamp — це час у форматі epoch у мілісекундах, він не повинен бути в майбутньому і не повинен бути старшим за два дні. Генеруйте його на сервері в тому самому запиті, де обчислюється хеш. Генерація в браузері — класична помилка: значення відрізняється від того, що було підписано хешем, і кожен коментар відхиляється.

Ніколи не встановлюйте 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 // 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

Дві речі, які можуть зіпсувати

Перевірте сирі байти. Парсинг JSON та його повторна серіалізація змінює порядок ключів і пробіли, тому хеш відрізняється і кожна доставка провалюється без очевидної причини. Це звичайна причина, чому отримувач вебхука «просто не працює».

Порівнюйте за постійний час. Проста операція === над підписом розкриває, скільки байтів збіглося, чого достатньо, щоб підробити один байт за раз.

Обробка подій

Відповідайте швидко. FastComments повторює запит у випадку відповіді, відмінної від 2xx, і кінцева точка, яка постійно помиляється, зрештою автоматично вимикається, тому реальну роботу виконуйте після відповіді, а не вбудовано.

Зробіть цю роботу ідемпотентною за ідентифікатором коментаря. Повторна спроба підписується новим часовим міткою, і той самий ідентифікатор коментаря надходить знову при редагуванні та видаленні, тому немає стабільного критерію для дедуплікації.

Приклад Vals Internal Link

Чотири публічних валів, які ви можете реміксити, кожен охоплює одну частину цього посібника.

Blog with comments (live) — це Markdown‑блог з гілкою під кожним постом і підрахунком коментарів у масиві на індексі. Він працює відразу після реміксу, і одна змінна середовища вказує його на ваш власний обліковий запис.

SSO demo (live) входить відвідувача за допомогою їхнього облікового запису Val Town і передає цю ідентичність віджету, тому другий вхід не потрібен.

Webhook receiver (live) перевіряє HMAC‑підпис кожної доставки і зберігає події в SQLite. Має кнопку, яка підписує тестове навантаження і надсилає його самому собі, тож ви можете спостерігати успішну верифікацію перед налаштуванням реального вебхука.

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

Ті ж навички можна встановити будь-де за допомогою npx skills add fastcomments/skills.