prelo
Plugins / Blog comments

Blog comments

Threaded comments with a moderation queue in Studio. No third-party embed.

Mgithub.com/mikkohContentprelo >= 0.4Updated May 2026
Use this plugin
1
2
or point your agent at the spec:
Your agent reads the spec and writes the code into your project. Review the diff like any change.

What this adds

Comments under your blog posts — threaded one level deep, with a moderation queue the site owner works through in Studio. Comments are stored in your own database and rendered by your own frontend: no Disqus iframe, no tracking scripts, no third-party account.

How it works

Comments are a content type. New comments arrive as drafts (the moderation queue); approving one is just publishing it, which is the only way it becomes publicly readable — the content API never exposes drafts. Two custom routes handle the public surface: one accepts new comments with honeypot + rate limiting, one returns the approved thread for a post.

The spec your agent followsexpand ▾
You are adding native comments to a Prelo project's blog.

1. **Content type.** In `cms.config.js`:

   ```js
   comment: {
     label: "Comments",
     fields: {
       post:    { type: "text", required: true },  // slug of the post commented on
       author:  { type: "text", required: true },  // display name
       email:   { type: "text", required: true },  // never rendered publicly
       body:    { type: "text", required: true },
       reply_to:{ type: "number" },                // parent comment id, one level max
     },
     studio: { columns: ["post", "author", "body"] },
   },
   ```

   Draft = pending moderation. Published = approved and visible. Delete = rejected/spam.

2. **Rate limiting.** `hooks.boot` creates `x_comment_hits (ip TEXT, at INTEGER)`; limit 3 comments per 10 minutes per IP, pruning old rows each request.

3. **Routes** (`routes:` in `cms.config.js`, mounted under `/api/x/`):
   - `"POST /comments"` — validate: `post` matches an existing **published** post slug (look it up; 404 otherwise), `author` ≤ 80 chars, `email` well-formed, `body` 1–4000 chars, `reply_to` (if set) is an existing **published** comment on the same post whose own `reply_to` is empty (enforces one-level threading). Honeypot field `website`: silently accept and discard. Insert as a draft `comment` row (title = author, fields in the JSON column, matching the shape the content API writes). Respond `{"ok":true,"queued":true}`.
   - `"GET /comments/:slug"` — return published comments for the post, oldest first, as `{id, author, body, reply_to, at}` — **never include email**. This route exists because the public generic API would also work (`GET /api/comment` returns published only) but would leak the email field; the custom route is the field filter.

4. **Frontend.** Under each post:
   - Render the thread from `GET /api/x/comments/{slug}`: top-level comments in order, replies nested one level, relative timestamps, author name in the project's text styles. Escape everything — comment bodies are plain text, render newlines as paragraphs, no HTML pass-through.
   - A comment form (name, email, comment, hidden `website` honeypot) posting to `/api/x/comments`. Success state: "Thanks — your comment is awaiting moderation." Keep input on failure. A "Reply" link under each top-level comment reveals the same form with `reply_to` set.
   - Server-render the thread where the frontend architecture allows so comments are crawlable.

5. **Optional notification.** If SMTP env is configured, email the site owner on each new pending comment (subject: post title). Skip silently otherwise.

6. **Verify.** `prelo check`; post a comment, see it as a draft in Studio, publish it, confirm it renders and that `GET /api/comment/{id}` public response is the only other exposure (and decide with the user whether to keep that type public — if not, note the email-leak consideration above is already handled by the custom route).

After it's built

Moderation is the Studio list: pending comments are drafts, publish to approve, delete to reject. The owner can edit a comment's body before approving (typos, stripping a URL) like any other content.