FastComments.com

Val Town 앱에 실시간 댓글 추가

Val Town 은 Deno에서 TypeScript를 실행하므로 val은 실제 서버입니다. 이는 FastComments에 적합합니다: 위젯은 페이지에 삽입되는 script 태그이며, Secure SSO나 웹훅 검증처럼 비밀이 필요한 모든 작업을 동일한 val에서 서버 측으로 실행할 수 있습니다.

이 가이드는 HTTP val에 댓글 위젯을 추가하고, 인덱스 페이지에 댓글 수를 표시하며, 사용자가 이미 가지고 있는 Val Town 계정으로 로그인하고, 댓글 웹훅을 받는 방법을 다룹니다.

시도하는 데 계정이 필요하지 않습니다. 예제는 tenantId: "demo" 를 사용하며, 공유 샌드박스이며, 2단계에서 자신의 것으로 전환하는 방법을 설명합니다.

인덱스 페이지의 댓글 수 Internal Link

인덱스 페이지에서는 행마다 하나의 comment-count 위젯을 렌더링하지 마세요. 이는 게시물당 하나의 요청이 발생합니다. 전체 페이지에 대해 단일 요청으로 처리되는 bulk count를 사용하세요.

각 행에 해당 스레드가 사용하는 urlId를 표시하고, bulk 위젯을 한 번만 로드하세요:

인덱스 페이지의 대량 댓글 수
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와 일치해야 합니다. 위젯이 슬러그를 사용한다면, 마커도 슬러그를 사용합니다. 일치하지 않을 경우 댓글이 있는 스레드에서도 0이 표시됩니다.

스크립트는 window.FastCommentsBulkCountConfig를 폴링하므로, 스크립트 태그 앞이나 뒤에 설정을 넣는 것이 중요하지 않습니다.

std/oauth를 사용한 보안 SSO Internal Link

이미 방문자가 누구인지 val이 알고 있다면, Secure SSO는 해당 신원을 위젯에 전달하여 두 번째 로그인을 보지 않도록 합니다. 만들 엔드포인트도 없고 런타임에 호출할 것도 없습니다: 서버 측에서 세 개의 값을 계산하여 위젯 설정에 전달하면 됩니다.

Val Town은 std/oauth를 통해 설정 없이 바로 사용할 수 있는 로그인을 제공하므로, 방문자는 이미 가지고 있는 Val Town 계정으로 로그인할 수 있습니다. 이를 여러분의 앱에서 사용하는 것으로 바꿔도 FastComments 쪽은 변경되지 않습니다.

서버에서 페이로드 빌드하기

API 시크릿은 페이로드에 서명하며 브라우저 코드에 절대 노출되어서는 안 됩니다. npm에서 SDK를 설치하세요. Val Town의 Deno 런타임에서 그대로 작동합니다:

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를 전송하는 여러분만의 작은 라우트를 가리키도록 하세요.

방문자가 로그아웃 상태일 때는 loginURL만 포함된 sso를 전달하세요. 그러면 위젯은 익명 댓글 상자 대신 로그인 프롬프트를 표시합니다.

자주 발생하는 문제

timestamp는 에포크 밀리초 단위여야 하고, 미래 시점이어서는 안 되며, 이틀 이상 오래되어서도 안 됩니다. 해시를 계산하는 동일한 요청에서 서버에서 생성하세요. 브라우저에서 생성하는 것이 전형적인 실패 사례입니다: 값이 해시된 값과 달라져 모든 댓글이 거부됩니다.

신원 제공자(identity provider)에서 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가 아닌 응답에 대해 재시도하며, 계속 실패하는 엔드포인트는 결국 자동으로 비활성화됩니다. 따라서 응답을 반환한 후에 실제 작업을 수행하고, 인라인으로 처리하지 마세요.

해당 작업을 댓글 ID에 대해 멱등하도록 만드세요. 재시도 시 새로운 타임스탬프로 다시 서명되며, 편집 및 삭제 시 동일한 댓글 ID가 다시 도착하므로 중복 제거를 위한 안정적인 기준이 없습니다.

예시 Vals Internal Link

이 가이드의 각 부분을 다루는 리믹스 가능한 공개 val 네 개입니다.

Blog with comments (라이브)는 모든 게시물 아래에 댓글 스레드가 있고 인덱스에는 대량 댓글 수가 표시되는 Markdown 블로그입니다. 리믹스하는 즉시 작동하며, 환경 변수 하나만 설정하면 본인 계정으로 연결됩니다.

SSO demo (라이브)는 방문자를 Val Town 계정으로 로그인시키고 해당 신원을 위젯에 전달하여 두 번째 로그인이 필요 없게 합니다.

Webhook receiver (라이브)는 모든 전송에서 HMAC 서명을 검증하고 이벤트를 SQLite에 저장합니다. 테스트 페이로드에 서명하여 자기 자신에게 전송하는 버튼이 있어, 실제 웹훅을 설정하기 전에 검증이 성공하는 것을 확인할 수 있습니다.

Agent skills (라이브)는 위젯, SSO, REST API, 모더레이션, Disqus 마이그레이션을 다루는 FastComments 에이전트 스킬 라이브러리입니다. 리믹스하면 Val Town의 에이전트인 Townie가 skills/ 디렉터리에서 스킬을 자동으로 가져오므로, 문서를 채팅에 붙여넣지 않아도 에이전트가 댓글 설정 방법을 알게 됩니다.

동일한 스킬은 npx skills add fastcomments/skills로 어디서든 설치할 수 있습니다.