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 не змінюється.

Build the payload on the server

Секрет 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 }. Ці три значення — це все, що потрапляє в браузер. Секрет підписує їх, а потім відкидається, тому нічого на сторінці не дозволяє читачу підробити інший користувач.

Pass it to the 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 // ...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. Тоді віджет покаже запит на вхід замість анонімного поля коментаря.

Things that go wrong

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

Ніколи не встановлюйте isAdmin або isModerator з провайдера ідентифікації. Вхід за допомогою облікового запису Val Town нічого не говорить про те, хто повинен модерувати ваш сайт.

Перегляньте посібник SSO для повного списку полів, потоків з груповим доступом та значків.

Отримання вебхукiв 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.