Comments
Threaded comments on product pages with a multi-layered spam + profanity filter.
Every product page has a comment section. Authenticated users post; admins moderate.
Thread shape
Strictly one level deep. The schema allows parent_id, but the server action explicitly rejects replies to replies with "Cannot reply to a reply" (lib/actions/comment.ts).
Comment A
├─ Reply A1
├─ Reply A2
Comment B
└─ Reply B1Posting flow
- Signed-in user types → submits
- Server action runs moderation checks (see below)
- If clean: insert with
status = 'approved' - If flagged: depends on the check — some auto-
rejected, some blocked entirely
Moderation checks
lib/moderation.ts runs multiple checks:
Profanity filters (26 languages total)
- English uses the
obscenitylibrary with evasion detection (leetspeak, unicode tricks, unusual spacing) - 25 other languages via
leo-profanity: French, Spanish, German, Italian, Portuguese, Russian, Arabic, Chinese, Japanese, Korean, Dutch, Polish, Turkish, Thai, Vietnamese, Indonesian, Swedish, Danish, Finnish, Norwegian, Czech, Hungarian, Romanian, Ukrainian, Hindi
Spam heuristics
- Link spam: more than 2 URLs in the body
- Character repetition: 8+ of the same character in a row (e.g. "aaaaaaaa")
- Shouting: more than 70% uppercase letters when the body is longer than 20 chars
- Duplicate word spam: the same word repeated 5+ times AND making up >50% of the message
Rate limit
Per-user, configured via settings:
comments_per_windowcomments_window_seconds
IP tracking
Every comment stores the submitter IP (x-forwarded-for) in comments.ip. Useful for abuse audits — admin can sort / filter by IP via SQL.
Moderation UI
/admin/comments lets you:
- Filter by status (approved / rejected)
- Search by body or author
- Per-comment approve / reject / delete buttons
No bulk actions and no keyboard shortcuts. Each comment is handled individually.
Status transitions
commentStatus enum values: approved | rejected.
- Default insert status is
approved(orrejectedif moderation flagged something severe) - Admin reject sets
status = 'rejected' - Admin approve flips back to
approved - Admin delete removes the row entirely (hard delete — no audit trail)
Rendering
Comments render inline on /p/[slug] under the product body. Only approved ones show on the public page.
Disabling comments
To turn off comments site-wide:
- Remove the
<Comments>component fromapp/p/[slug]/page.tsx - Optionally drop the
commentstable in your schema