Enabling Blog Comments in emdash
One of the things we keep discovering about emdash is how much is already built in. Today we wanted to add comments to the blog. We expected to build a plugin, wire up a database table, design an API. Instead, we found that emdash already ships a complete comments system. We just needed to turn it on.
What emdash provides out of the box
The comments infrastructure lives in emdash core, not a plugin. Migration 027 creates the _emdash_comments table with everything you'd expect: threading via parent_id, moderation statuses (pending, approved, spam, trash), IP hashing for rate limiting, and support for both anonymous and authenticated commenters.
Each collection has its own comment settings: enable/disable, moderation mode (none, first_time, or all), auto-close after N days, and auto-approve for CMS users. The defaults are sensible: first-time moderation with a 90-day window.
The API routes are already registered:
Public: GET and POST at /_emdash/api/comments/:collection/:contentId for listing approved comments and submitting new ones.
Admin: GET at /_emdash/api/admin/comments for the moderation inbox, and PUT at /_emdash/api/admin/comments/:id/status for approving or rejecting.
Anti-spam is built in too. The POST endpoint expects a honeypot field (website_url) that's hidden from real users but filled by bots. If it's populated, the API silently accepts the request but discards it. Rate limiting uses the _emdash_rate_limits table keyed on hashed IPs.
Enabling comments
Comments are disabled by default on all collections. Enabling them is a single D1 query:
UPDATE _emdash_collections SET comments_enabled = 1 WHERE slug = 'posts';
Run that via wrangler d1 execute against the remote database and the API starts accepting comments immediately. No redeploy needed for the backend change.
Building the frontend component
The backend is there, but emdash doesn't ship a frontend comments component. That's by design: your site's UI is your own. We built a Comments.astro component that handles both rendering and submission.
For rendering, emdash exports getComments() and getCommentCount() from the main package. These are server-side functions that query the database during Astro's SSR pass, same pattern as getMenu() or getEmDashCollection(). Call getComments() with threaded: true and you get back a nested structure with replies attached to their parent comments.
For submission, the component includes a vanilla JS form handler that POSTs JSON to the comments API endpoint. The form collects name, email, and comment body, plus the hidden honeypot field. On success, it shows either "Comment posted!" (auto-approved) or "Your comment is awaiting moderation" (pending review).
Threading is supported with a Reply button on each top-level comment. Clicking it sets a parentId in the form and shows a "Replying to [name]" banner with a cancel button. emdash enforces one level of nesting: if you reply to a reply, it automatically attaches to the root comment instead of creating deeper threads.
One gotcha: Astro entry IDs vs database IDs
We hit one issue during testing. The comment form was getting "Content not found" errors when submitting. The problem: we were passing post.id to the API, but in Astro's content layer, entry.id is a slug-based identifier, not the database ULID. The actual database ID lives at post.data.id. Switching to post.data.id fixed it immediately.
This is worth remembering for any emdash feature that needs to reference content by database ID (comments, SEO lookups, etc.): always use the data.id property, not the Astro entry ID.
The moderation flow
With moderation set to "first_time", the flow works like this: a new commenter's first comment goes to pending status. Once you approve it, all subsequent comments from the same email address are auto-approved. CMS users (logged into the admin) are always auto-approved.
Right now we're approving comments via D1 SQL, which is fine for a low-traffic blog but not a long-term solution. The admin API exists, so the next step is either building a small moderation page or using emdash's built-in admin panel.
The plugin hook system
One of the more interesting architectural decisions: comment moderation uses emdash's plugin hook system. The built-in moderator registers as a comment:moderate exclusive hook with a simple decision tree. But any plugin can replace it by registering its own exclusive handler for the same hook.
This means you could build an AI moderation plugin that classifies comments using an LLM, or an Akismet integration, or any custom logic, and it drops right into the existing pipeline. The comment:beforeCreate hook lets plugins transform or reject comments before they reach the moderator. And comment:afterCreate fires after the comment is saved, useful for notifications or analytics.
Email notifications to content authors are already wired into the core. When a comment is approved (either auto-approved or manually), emdash checks if the content has an author with a verified email and sends a notification through the email pipeline. Since we already have the Postmark email plugin running, this should work out of the box.
What's next
The comments system is functional but minimal. Here's what we're considering:
Admin moderation UI. A simple page to review, approve, and reject comments without touching D1 directly.
AI spam moderation. Replace the default moderator with an LLM-based classifier via the comment:moderate hook. The plugin architecture makes this straightforward.
Comment counts on the blog listing. Show how many comments each post has on the index page using getCommentCount().
Gravatar support. Hash the author email for avatar URLs. Small touch that makes comment threads feel more personal.
Markdown rendering. Allow basic formatting in comment bodies (bold, italic, links, code blocks).
The pattern here is the same one we keep finding with emdash: the foundation is solid and well-designed, and extending it follows clean, documented conventions. We went from "let's build a comments plugin" to "oh, it's already there" to live and working in under an hour. That's a good CMS.
Comments
No comments yet. Be the first to share your thoughts.
Leave a comment