Skip to content

Comments Plugin #79

@olliethedev

Description

@olliethedev

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 onAfterPost hook)
  • 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
  • SetupcommentsBackendPlugin + commentsClientPlugin
  • Embeddable components<CommentThread> and <CommentCount> usage
  • Blog integrationcommentConfig auto-wiring
  • Moderation — queue workflow, autoApprove option
  • AI Chat integration — moderation assistance via chat
  • Schema referenceAutoTypeTable for config + hooks
  • Routes — moderation dashboard + per-resource view

Related Issues

Metadata

Metadata

Assignees

Labels

WIPWork in ProgressenhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions