Commit 4ef2ce7
feat(v2.0c): comments — third slice of v2.0 engagement (#45)
Per-article visitor comments with editor moderation. Dual-toggle
policy so a fresh deploy never accidentally exposes a comment form.
Schema (Drizzle migration 0009)
-------------------------------
- comments: id, articleId FK CASCADE, parentId (forward-compat for
threading; UI is flat), authorName, authorEmail (collected, never
displayed publicly), body (plain text — never markdown to keep
XSS surface minimal), status enum (pending/approved/spam/
archived), ipHash (16-char SHA-256 truncate; never raw IP),
submittedAt, moderatedBy + moderatedAt.
- articles.commentsMode: new column. enum (inherit/on/off),
default 'inherit'.
Dual toggle
-----------
- Site-wide: settings.commentsEnabled defaults to false. New
Comments card in /cms/settings.
- Per-article: radio (Inherit / On / Off) on the article form.
- $lib/server/comments.commentsAllowedForArticle() is the one-line
truth table both the public render and the POST endpoint use.
Public surface
--------------
- POST /api/comments. Reuses v2.0a hashIp + rate-limit (3/min per
ipHash per article). Honeypot _hp via $lib/forms/constants
(extracted to a non-server module so client + server share the
field name). 410 when commentsAllowed=false; 429 on rate limit.
- (www)/[locale]/blog/[slug] page-server load now also fetches
approved comments + computes commentsOpen.
- New CommentSection.svelte renders approved comments + a
fetch()-posted submission form. Fields: name (required, ≤80),
email (required, ≤254, server-side regex), body (required,
≤4000). Plain-text rendering with whitespace-pre-wrap.
- Page svelte mounts CommentSection when there are approved
comments OR when commentsOpen=true.
CMS surface
-----------
- /cms/comments moderation queue. Status tabs (pending/approved/
spam/archived). Each row: status badge, timestamp, author + masked
email (a***@e***.com via $lib/comments/mask), linked article
title (batch-resolved per page), body, mark-as buttons (approved/
spam/archived), reply-via-email mailto, delete. Pagination
50/page. Pending count exposed for future sidebar badge.
- Sidebar entry under taxonomy group, gated to editor+.
Audit
-----
- New comment.{create,approve,spam,archive,delete} AuditAction
members.
- Public submission writes comment.create with userId=null.
- Moderation actions write the matching status-change action with
the editor's userId.
Constants
---------
- HONEYPOT_FIELD + RATE_LIMIT_WINDOW_SECONDS + RATE_LIMIT_MAX_PER_
WINDOW moved from $lib/server/forms (server-only) to $lib/forms/
constants so client components can import them. Server module
re-exports for backwards compat.
i18n: 26 new comment_* / comments_* / cms_settings_comments_* keys
(EN + TH).
Migration 0009 already applied to live D1.
Out of scope (deliberate non-goals for this slice):
- Threaded replies — parentId is forward-compat only; UI is flat.
- Comments on Pages — Pages are typically static.
- Akismet/ML spam filtering — honeypot+rate-limit is the v2.0 floor.
- Email notifications when comment approved.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent 3f52dbc commit 4ef2ce7
29 files changed
Lines changed: 3315 additions & 515 deletions
File tree
- docs
- drizzle
- meta
- messages
- src
- lib
- comments
- components
- cms
- comments
- editor 2
- forms
- server
- audit
- comments
- content
- providers
- forms
- routes
- (cms)/cms
- articles
- [id]
- new
- comments
- settings
- (www)/[locale]/blog/[slug]
- api/comments
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
287 | | - | |
| 287 | + | |
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | | - | |
| 251 | + | |
252 | 252 | | |
253 | 253 | | |
254 | 254 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
0 commit comments