Threaded comments with a moderation queue in Studio. No third-party embed.
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.
Comments are a private content type — the generic content API never serves it, so commenter emails stay in your database. New comments arrive as drafts (the moderation queue); approving one is just publishing it. Two custom routes handle the public surface: one accepts new comments with honeypot + rate limiting, one returns the approved thread for a post with the email field stripped.
You are adding native comments to a Prelo project's blog.
1. **Content type.** In `cms.config.js`:
```js
comment: {
label: "Comments",
access: "private", // approved comments carry emails — read via the custom route only
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.
`access: "private"` is **required**: rows store the commenter's email, and without it every approved (published) comment — email included — would be readable by anyone at `GET /api/comment`. The custom route in step 3 is the only public read surface, and it filters the fields.
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**. Query the `content` table directly (the type is `access: "private"`, so the generic public API is off); this route is the only public read surface and 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 via the custom route, and confirm unauthenticated `GET /api/comment` returns `404 unknown_type` — commenter emails must never be publicly readable.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.
Events as a content type with a monthly calendar view and iCal feed.
Instant client-side search across pages and posts, indexed at build time.
Appointment requests with availability windows and email confirmation.