-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Overview
Add a Comments plugin that attaches threaded discussions to any content in the stack — blog posts, CMS entries, product pages, or any custom page. It is designed to be composable, not standalone: consumers bind comments to a resource by passing a resourceId and resourceType string.
Think a lightweight Disqus or Giscus replacement that is fully self-hosted and integrates natively with the AI Chat plugin for moderation assistance.
Core Features
Public Facing
- Comment list with nested replies (one level of threading in v1)
- Post a comment (name + email optional, or auth-gated via lifecycle hook)
- Reply to a comment
- Like / upvote a comment
- Pagination / "load more" (infinite scroll)
Moderation (Admin)
- Moderation queue — approve / reject / spam pending comments
- Auto-approve toggle (default: off — all comments pending until approved)
- Bulk approve / bulk delete
- Per-resource comment dashboard (view all comments for a given page/post)
Integration
- Blog plugin — comment count badge on post list; full thread on post detail
- CMS plugin — attach comments to any content entry via
resourceId - AI Chat — "summarize the discussion", "flag spam comments", moderation via chat
Schema
import { createDbPlugin } from "@btst/stack/plugins/api"
export const commentsSchema = createDbPlugin("comments", {
comment: {
modelName: "comment",
fields: {
resourceId: { type: "string", required: true }, // e.g. post slug, CMS entry ID
resourceType: { type: "string", required: true }, // e.g. "blog-post", "cms-entry"
parentId: { type: "string", required: false }, // null = top-level, string = reply
authorName: { type: "string", required: false },
authorEmail: { type: "string", required: false },
body: { type: "string", required: true },
status: { type: "string", defaultValue: "pending" }, // "pending" | "approved" | "spam"
likes: { type: "number", defaultValue: 0 },
createdAt: { type: "date", defaultValue: () => new Date() },
updatedAt: { type: "date", defaultValue: () => new Date() },
},
},
})Plugin Structure
src/plugins/comments/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin — comment + moderation endpoints
│ ├── getters.ts # listComments, getComment, getCommentCount
│ ├── mutations.ts # createComment, approveComment, deleteComment
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — moderation dashboard route
├── overrides.ts # CommentsPluginOverrides
├── index.ts
├── hooks/
│ ├── use-comments.tsx # useComments, useCommentCount, usePostComment
│ └── index.tsx
└── components/
├── comment-thread.tsx # Embeddable thread — used in blog/cms pages
├── comment-form.tsx # Post / reply form
├── comment-count.tsx # Lightweight badge component
└── pages/
├── moderation-page.tsx / .internal.tsx # Admin moderation queue
└── resource-comments-page.tsx / .internal.tsx # Per-resource view
Routes
| Route | Path | Description |
|---|---|---|
moderation |
/comments/moderation |
Admin queue — pending, approved, spam |
resourceComments |
/comments/:resourceType/:resourceId |
Per-resource comment view |
Embeddable Components
The key consumer-facing surface is not the admin route — it is the drop-in components:
import { CommentThread, CommentCount } from "@btst/stack/plugins/comments/client"
// On a blog post page:
<CommentThread
resourceId={post.slug}
resourceType="blog-post"
apiBaseURL="https://example.com"
apiBasePath="/api/data"
/>
// Badge on post list cards:
<CommentCount
resourceId={post.slug}
resourceType="blog-post"
apiBaseURL="https://example.com"
apiBasePath="/api/data"
/>Blog Plugin Integration
On post-page.internal.tsx, below the post body:
import { CommentThread } from "@btst/stack/plugins/comments/client"
// When the comments plugin is registered, the blog overrides type accepts a commentConfig:
blog: blogClientPlugin({
...config,
commentConfig: {
apiBaseURL: config.apiBaseURL,
apiBasePath: config.apiBasePath,
},
})The blog plugin checks for commentConfig and renders <CommentThread> automatically — zero extra wiring for consumers who have both plugins registered.
Hooks
commentsBackendPlugin({
autoApprove?: boolean // default: false
onBeforePost?: (comment, ctx) => Promise<void> // throw to reject (e.g. auth check, profanity filter)
onAfterPost?: (comment, ctx) => Promise<void> // send notification email
onAfterApprove?: (comment, ctx) => Promise<void>
onAfterDelete?: (comment, ctx) => Promise<void>
})AI Chat Integration
The moderation page registers AI context for bulk actions:
useRegisterPageAIContext({
routeName: "comments-moderation",
pageDescription: `${pendingCount} comments awaiting moderation.\n\nTop pending:\n${pending.slice(0, 5).map(c => `- "${c.body}" by ${c.authorName}`).join("\n")}`,
suggestions: ["Approve all safe-looking comments", "Flag spam comments", "Summarize today's discussion"],
})Backend API Surface
const comments = await myStack.api.comments.listComments({ resourceId: "my-post", resourceType: "blog-post", status: "approved" })
const count = await myStack.api.comments.getCommentCount({ resourceId: "my-post", resourceType: "blog-post" })SSG Support
Comment threads are dynamic (user-generated content that changes frequently) — prefetchForRoute is not applicable. Use dynamic = "force-dynamic" on pages with embedded threads, or fetch comment counts at build time and revalidate via ISR.
Consumer Setup
// lib/stack.ts
import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api"
comments: commentsBackendPlugin({
autoApprove: false,
onAfterPost: async (comment) => {
// send moderation notification email
},
})// lib/stack-client.tsx
import { commentsClientPlugin } from "@btst/stack/plugins/comments/client"
comments: commentsClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBasePath: "/pages",
queryClient,
})Non-Goals (v1)
- Rich text / markdown in comments (plain text only)
- Email notifications to commenters on reply (use
onAfterPosthook) - OAuth / social login for commenters
- Comment reactions beyond likes
- Real-time updates (polling only)
- Webhooks
Plugin Configuration Options
| Option | Type | Description |
|---|---|---|
autoApprove |
boolean |
Skip moderation queue (default: false) |
hooks |
CommentsPluginHooks |
onBeforePost, onAfterPost, onAfterApprove, onAfterDelete |
Documentation
Add docs/content/docs/plugins/comments.mdx covering:
- Overview — composable, resource-scoped, self-hosted
- Setup —
commentsBackendPlugin+commentsClientPlugin - Embeddable components —
<CommentThread>and<CommentCount>usage - Blog integration —
commentConfigauto-wiring - Moderation — queue workflow,
autoApproveoption - AI Chat integration — moderation assistance via chat
- Schema reference —
AutoTypeTablefor config + hooks - Routes — moderation dashboard + per-resource view
Related Issues
- Job Board Plugin #58 Job Board Plugin
- Newsletter / Marketing Emails Plugin #75 Newsletter / Marketing Emails Plugin (notification emails on new comment)
- CRM Plugin #76 CRM Plugin
- Analytics Plugin #74 Analytics Plugin