Comments on the site's posts and pages, the way WordPress takes them: the list and the form under a record, the rules of Settings → Discussion, moderation and its emails. A site moved from WordPress keeps its comments and its discussion settings as they were, and WordPress reads what this module writes.
The admin is kitloom/comments-filament or kitloom/comments-nova: they draw Settings → Discussion and send the moderation emails' links to the admin's Comments screen, where comments are approved, answered, marked as spam or trashed. In both admins an answer is the Reply action on a comment — as in WordPress, there is no screen to add a comment of one's own; a moderator's reply approves a comment that waits.
Under a post — or a page, or any type that takes comments — the theme draws:
how many comments there are, and the comments themselves, replies under what they answer;
the form: the name, email and website of a guest (or "Logged in as … Log out?" for a signed-in user), the comment, the box "Save my name, email, and website in this browser for the next time I comment", and the spam protection the site has (kitloom/forms' honeypot and Cloudflare Turnstile, when installed);
links to older and newer comments when they are broken into pages;
the visitor's own comment while it waits for approval, marked "Your comment is awaiting moderation" — nobody else sees it;
nothing at all on a password-protected record (its text is for its editors only, and so are its comments), and no form when comments are closed.
Avatars are the commenter's initials: no Gravatar, so no visitor's email hash leaves the site.
WordPress's own screen, with only the settings this module carries out (no pingbacks, no avatars):
Setting
What it does
Allow people to submit comments on new posts
What a new record starts with; each record has its own "Allow comments" switch on its form. Pages start with comments closed, as in WordPress.
Comment author must fill out name and email
Guests only; a signed-in user is known by their account.
Users must be registered and logged in to comment
A guest sees a link to sign in instead of the form.
Automatically close comments on posts older than … days
Counted from publication. Posts only, unless close_comments_for_post_types says more. 0 days closes nothing.
Show comments cookies opt-in checkbox
Off — guests are never remembered.
Enable threaded (nested) comments … levels deep
2–10 levels. A reply deeper than that is shown after its parent at the last level, without a reply link. Off — one flat list, no reply links.
Break comments into pages with … top level comments per page
Pages count threads (a comment with all its replies), or every comment when not threaded.
… page displayed by default
newest — the record's address shows the last page; oldest — the first.
Comments … at the top of each page
older (asc) or newer (desc) threads first; replies always go oldest first.
Email me whenever anyone posts a comment
To the record's author, for an approved comment, unless they wrote or approved it themselves. A module's own type (kitloom/shop's product reviews) too, as WordPress mails them.
Email me whenever a comment is held for moderation
To the Administration Email Address (Settings → General) and the record's author — unless the type says the author is not asked (CommentType::authorModerates()): a product review (kitloom/shop) goes to the Administration Email Address only, as WooCommerce sends it.
Comment must be manually approved
Every comment waits.
Comment author must have a previously approved comment
A newcomer's first comment waits; once one is approved, the next ones go straight on. Known by their account, or by the same name and email.
Hold a comment in the queue if it contains … or more links
0 — links never hold a comment.
Comment Moderation
Words, IP addresses, emails, one per line: a comment with one in its text, name, email, website, IP or browser waits. Parts of words count ("press" holds "WordPress").
Disallowed Comment Keys
The same matching; such a comment goes straight to the trash.
The record's author and anyone who may moderate comments (administrators, editors) are approved outright, as in WordPress: neither the queue nor the keys apply to them.
In WordPress's order; the first rule that fails answers:
The record exists, is published (or private and readable by the user), its type takes comments, its "Allow comments" is on, it is not too old, and no password stands for this visitor.
An account, if Discussion asks for one; a name and an email from a guest, if Discussion asks for them.
The email's shape and the lengths (name 245, email 100, website 200, comment 65 525 characters).
A reply goes under an approved comment of the same record.
The spam protection, for a comment sent from a browser: a bot gets 403 and nothing is kept.
Not the same comment twice from the same author on the same record.
Not two comments within 15 seconds from the same visitor (address, account or email); moderators never wait.
A disallowed key sends it to the trash; manual approval, a moderation key, too many links or a newcomer hold it; otherwise it is approved.
A module with a comment type of its own declares it to kitloom/wp-schema (Contracts\CommentType, tagged CommentType::TAG): its name and label, whether the record's author moderates it, its stars. kitloom/shop sends its product reviews through the same rules (CommentRules::submit(..., type: 'review')): a product is not a type that takes comments, so only its own Enable reviews switch and a password close it; a review is never a reply, and its form's spam protection is asked under reviews.
Whatever happened, the visitor hears "approved" or "awaiting moderation": a comment sent to the trash says it waits, as WordPress tells a spammer nothing.
In WordPress's comments table (the site's prefix in front), comment_typecomment. The status, comment_approved, is 1 (approved), 0 (waiting), spam or trash; spam and trash remember the status they came from (_wp_trash_meta_status in commentmeta), so restoring brings it back. The record's comment_count counts its approved comments. Reviews (review, kitloom/shop) and order notes live in the same table and are never shown as comments.
Discussion's settings are WordPress's options of the same names (default_comment_status, comment_moderation, thread_comments_depth, …), read from a WordPress database as WordPress wrote them.
When a guest ticks the consent box, their name, email and website are remembered for a year in comment_author_{hash}, comment_author_email_{hash} and comment_author_url_{hash} (the hash stands for the site, as WordPress's COOKIEHASH), and the form comes filled in next time. A comment sent without the tick removes them. Unlike WordPress's they are encrypted by the application and not readable by the page's scripts. A visitor who has them — or who just sent a comment that waits — gets pages drawn for them, never from the page cache.
After a guest's comment that waits, the form leads to …?unapproved=ID&moderation-hash=…#comment-ID: for ten minutes that address shows them their comment, also without the cookies.
"[Site] Please moderate: "Title"" and "[Site] Comment: "Title"", as Discussion's "Email me whenever" asks for them, through the application's mailer and queue. Each says who wrote what, where, and links to approve, spam, trash or edit the comment in the admin — the admin package provides the links; they open the admin, where the moderator is signed in and acts, never from the email itself. A comment approved or trashed before the queue gets to its email is not mailed about.
With kitloom/health installed, Site Health warns when comments or reviews have waited for moderation more than 7 days, and fails when more than 50 wait (comments_awaiting_moderation; kitloom-health.checks.comments).
Fields with WordPress's REST names: post, parent (0 — not a reply), author_name, author_email, author_url, content, wp-comment-cookies-consent (any value — ticked), plus what the spam protection drew into the form. With the session and the CSRF token, as any form of the site (web group), and at most 5 comments a minute per visitor (routes.throttle).
With Accept: application/json:
Answer
When
201 {message, status, comment}
Taken. status is approved or hold; message is null or "Your comment is awaiting moderation."; comment is an item as the page's comments.items have it.
400 rest_invalid_param, data.params.{field}
A field failed: "Please fill the required fields.", "Please enter a valid email address.", "Please type your comment text.", "Your comment is too long.", "Invalid parent comment ID.", …
Errors are WordPress's JSON: {"code", "message", "data": {"status"}}.
A form posted without JavaScript goes to the comment (…#comment-ID, on the page of comments it is on) or, when it waits, there with ?unapproved=… and the notice; a refusal goes back to the page with the input, Laravel's validation errors and the notice.
With kitloom/ai installed, an agent working for a user gets:
Tool
Who gets it
What it does
comments_list
anyone who writes posts (edit_posts)
Comments newest first, by status (approved, hold, spam, trash; "all" is approved and waiting, as the Comments screen's), record, text, or product reviews (type: review) with their stars. A user who may not moderate sees only approved comments on the records they can see, never behind a password.
comments_moderate
moderators (moderate_comments)
Approve, hold, spam, trash or restore one comment or several. Nothing is deleted for good: emptying the trash is a person's, in the admin.
comments_reply
moderators
An approved reply under a comment or a review, in the user's name. Not under a comment still waiting: the agent approves it first with comments_moderate — a commenter's text can ask to be answered, and a reply must not publish it.
The commenters' emails, IP addresses and browsers reach an agent only when its user may moderate comments. describe_site tells the agent the types that take comments, the Discussion settings in force under their WordPress names (the moderation and disallowed keys counted, not listed) and how many comments wait in each status.
Everything goes through CommentWriter as the agent's user, so the emails and the activity log hear it as theirs. ai.register off — no tools.
Every singular page whose type takes comments carries the part comments (in JSON by default; a template asks for Kitloom\Comments\Site\Comments). The keys are always the same:
open bool — the form may be drawn (a guest may still have to sign in: form.mustLogIn)closedReason null | 'type' | 'closed' | 'old' | 'password'count approved commentsthreaded bool; maxDepth 1 when not threadedorder 'asc' | 'desc'page, pages this page of comments, how many (1, 1 when not paged)prevUrl older comments, nextUrl newer ones — null at either end; the default page is the bare permalinkitems[] id, parentId, depth (1 — top level), link (…#comment-ID), date (ISO 8601), dateText, author {name, url|null, initials, isPostAuthor}, paragraphs (plain text; a "\n" inside one is a line break), status 'approved' | 'hold', canReply, children (the same shape)form null when closed, else: action (the route's URL), postId, mustLogIn, loginUrl (the site's account page, else /login/), loggedInAs {name}|null, logoutUrl (the site's `logout` route with `redirect_to` the post, as `wp_logout_url(get_permalink())`: POST it with the CSRF token; null for a guest), requireNameEmail, cookiesOptIn, author {name, email, url} (the visitor's own, from their cookies), protection (FormProtection::form('comments'): {honeypot?: {nameFieldName, validFromFieldName, encryptedValidFrom}, turnstile?: {siteKey}}), maxLengths {author_name, author_email, author_url, content}notice null | {type: 'success'|'error', message} — after a form posted without JavaScript
The text is never HTML: print paragraphs as text (white-space: pre-line keeps the line breaks). Nothing in the part carries another person's email, IP address or browser. A reply form posts parent = the item's id; a theme moves its form under the comment and shows "Cancel reply".
Further pages: GET <permalink>/comment-page-2/ with Accept: application/json (?include=comments for the part alone). Pages are numbered from the oldest; an address past the last one is sent to the last (a 302), so the page cache never keeps a copy of the page under every number.
use Kitloom\Comments\Posting\CommentRules;$comment = app(CommentRules::class)->submit( ['post' => $post->getKey(), 'content' => 'Thanks!', 'author_name' => 'Ada', 'author_email' => '[email protected]'], $user, // null — a guest $request, // null — made in code: the spam protection is not asked, no IP);
It throws Kitloom\Comments\Posting\CommentRefused (errorCode, status, the message) or a ValidationException. It writes through Kitloom\WpSchema\Content\CommentWriter, which an admin reply or an import uses directly with a status of its own — the discussion rules are this module's, not the writer's.
Kitloom\WpSchema\Events\CommentPosted and CommentStatusChanged (with the actor) — from every comment model, whoever wrote it. The emails listen to them: a comment written through CommentWriter is mailed about, an import that should stay quiet writes its rows itself.
Kitloom\Site\Contracts\FormProtection — what keeps bots out (context comments); kitloom/forms binds its honeypot and Turnstile, without it everything passes.
Kitloom\Comments\Contracts\ModerationLinks — links(Comment): {approve?, spam?, trash?, edit?} for the emails; the admin packages bind theirs (bind, over the module's bindIf of an empty one).
Kitloom\Comments\DiscussionOptions declares the options (group Discussion); Kitloom\Comments\Discussion reads them as what they mean; Kitloom\Comments\Commentable says which types take comments and why a record is closed.
Comments
Comments on posts and pages as WordPress takes them: the list and the form on the page, Settings → Discussion, moderation and its emails.
kitloom/comments· admins:kitloom/comments-filament,kitloom/comments-novaComments on the site's posts and pages, the way WordPress takes them: the list and the form under a record, the rules of Settings → Discussion, moderation and its emails. A site moved from WordPress keeps its comments and its discussion settings as they were, and WordPress reads what this module writes.
The admin is kitloom/comments-filament or kitloom/comments-nova: they draw Settings → Discussion and send the moderation emails' links to the admin's Comments screen, where comments are approved, answered, marked as spam or trashed. In both admins an answer is the Reply action on a comment — as in WordPress, there is no screen to add a comment of one's own; a moderator's reply approves a comment that waits.
What a visitor sees
Under a post — or a page, or any type that takes comments — the theme draws:
Avatars are the commenter's initials: no Gravatar, so no visitor's email hash leaves the site.
Settings → Discussion
WordPress's own screen, with only the settings this module carries out (no pingbacks, no avatars):
close_comments_for_post_typessays more. 0 days closes nothing.newest— the record's address shows the last page;oldest— the first.older(asc) ornewer(desc) threads first; replies always go oldest first.CommentType::authorModerates()): a product review (kitloom/shop) goes to the Administration Email Address only, as WooCommerce sends it.The record's author and anyone who may moderate comments (administrators, editors) are approved outright, as in WordPress: neither the queue nor the keys apply to them.
How a comment is taken
In WordPress's order; the first rule that fails answers:
A module with a comment type of its own declares it to kitloom/wp-schema (
Contracts\CommentType, taggedCommentType::TAG): its name and label, whether the record's author moderates it, its stars. kitloom/shop sends its product reviews through the same rules (CommentRules::submit(..., type: 'review')): a product is not a type that takes comments, so only its own Enable reviews switch and a password close it; a review is never a reply, and its form's spam protection is asked underreviews.Whatever happened, the visitor hears "approved" or "awaiting moderation": a comment sent to the trash says it waits, as WordPress tells a spammer nothing.
Where comments are kept
In WordPress's
commentstable (the site's prefix in front),comment_typecomment. The status,comment_approved, is1(approved),0(waiting),spamortrash; spam and trash remember the status they came from (_wp_trash_meta_statusincommentmeta), so restoring brings it back. The record'scomment_countcounts its approved comments. Reviews (review, kitloom/shop) and order notes live in the same table and are never shown as comments.Discussion's settings are WordPress's options of the same names (
default_comment_status,comment_moderation,thread_comments_depth, …), read from a WordPress database as WordPress wrote them.Cookies
When a guest ticks the consent box, their name, email and website are remembered for a year in
comment_author_{hash},comment_author_email_{hash}andcomment_author_url_{hash}(the hash stands for the site, as WordPress'sCOOKIEHASH), and the form comes filled in next time. A comment sent without the tick removes them. Unlike WordPress's they are encrypted by the application and not readable by the page's scripts. A visitor who has them — or who just sent a comment that waits — gets pages drawn for them, never from the page cache.After a guest's comment that waits, the form leads to
…?unapproved=ID&moderation-hash=…#comment-ID: for ten minutes that address shows them their comment, also without the cookies.Emails
"[Site] Please moderate: "Title"" and "[Site] Comment: "Title"", as Discussion's "Email me whenever" asks for them, through the application's mailer and queue. Each says who wrote what, where, and links to approve, spam, trash or edit the comment in the admin — the admin package provides the links; they open the admin, where the moderator is signed in and acts, never from the email itself. A comment approved or trashed before the queue gets to its email is not mailed about.
With kitloom/health installed, Site Health warns when comments or reviews have waited for moderation more than 7 days, and fails when more than 50 wait (
comments_awaiting_moderation;kitloom-health.checks.comments).The route
Fields with WordPress's REST names:
post,parent(0 — not a reply),author_name,author_email,author_url,content,wp-comment-cookies-consent(any value — ticked), plus what the spam protection drew into the form. With the session and the CSRF token, as any form of the site (webgroup), and at most 5 comments a minute per visitor (routes.throttle).With
Accept: application/json:{message, status, comment}statusisapprovedorhold;messageis null or "Your comment is awaiting moderation.";commentis an item as the page'scomments.itemshave it.rest_invalid_param,data.params.{field}not_logged_incomment_closed,comment_on_password_protected,comment_reply_to_unapproved_comment,comment_spamrest_post_invalid_idcomment_duplicate, 429comment_flood(or Laravel's throttle)Errors are WordPress's JSON:
{"code", "message", "data": {"status"}}.A form posted without JavaScript goes to the comment (
…#comment-ID, on the page of comments it is on) or, when it waits, there with?unapproved=…and the notice; a refusal goes back to the page with the input, Laravel's validation errors and the notice.AI agents
With kitloom/ai installed, an agent working for a user gets:
comments_listedit_posts)approved,hold,spam,trash; "all" is approved and waiting, as the Comments screen's), record, text, or product reviews (type: review) with their stars. A user who may not moderate sees only approved comments on the records they can see, never behind a password.comments_moderatemoderate_comments)comments_replycomments_moderate— a commenter's text can ask to be answered, and a reply must not publish it.The commenters' emails, IP addresses and browsers reach an agent only when its user may moderate comments.
describe_sitetells the agent the types that take comments, the Discussion settings in force under their WordPress names (the moderation and disallowed keys counted, not listed) and how many comments wait in each status.Everything goes through
CommentWriteras the agent's user, so the emails and the activity log hear it as theirs.ai.registeroff — no tools.Configuration
config/kitloom-comments.php(php artisan vendor:publish --tag=kitloom-comments-config):typessupports: comments(posts and pages).close_comments_for_post_types['post']flood_intervalroutes.registerroutes.throttle'5,1'notifications.registerai.registerfilament.register,nova.registerFor developers
The page part
Every singular page whose type takes comments carries the part
comments(in JSON by default; a template asks forKitloom\Comments\Site\Comments). The keys are always the same:The text is never HTML: print
paragraphsas text (white-space: pre-linekeeps the line breaks). Nothing in the part carries another person's email, IP address or browser. A reply form postsparent= the item'sid; a theme moves its form under the comment and shows "Cancel reply".Further pages:
GET <permalink>/comment-page-2/withAccept: application/json(?include=commentsfor the part alone). Pages are numbered from the oldest; an address past the last one is sent to the last (a 302), so the page cache never keeps a copy of the page under every number.Posting from code
It throws
Kitloom\Comments\Posting\CommentRefused(errorCode,status, the message) or aValidationException. It writes throughKitloom\WpSchema\Content\CommentWriter, which an admin reply or an import uses directly with a status of its own — the discussion rules are this module's, not the writer's.Events and contracts
Kitloom\WpSchema\Events\CommentPostedandCommentStatusChanged(with the actor) — from every comment model, whoever wrote it. The emails listen to them: a comment written throughCommentWriteris mailed about, an import that should stay quiet writes its rows itself.Kitloom\Site\Contracts\FormProtection— what keeps bots out (contextcomments); kitloom/forms binds its honeypot and Turnstile, without it everything passes.Kitloom\Comments\Contracts\ModerationLinks—links(Comment): {approve?, spam?, trash?, edit?}for the emails; the admin packages bind theirs (bind, over the module'sbindIfof an empty one).Kitloom\Comments\DiscussionOptionsdeclares the options (groupDiscussion);Kitloom\Comments\Discussionreads them as what they mean;Kitloom\Comments\Commentablesays which types take comments and why a record is closed.Tests
From the application that installs the package:
php vendor/bin/phpunit --testsuite Comments.