WebProCMS ships with a built-in blog comment system. It's a free core feature — not a paid add-on — and works on every install. Comments require social sign-in (Google, GitHub, Facebook, X, Microsoft, Apple, Discord) — there are no anonymous comments and no passwords to manage. Commenters' avatars are pulled automatically from their social profile. Moderators get a dedicated queue with approve / spam / trash / ban / whitelist actions, three moderation policies, and an optional email digest.
What you get
- Social sign-in only. Visitors comment after signing in with a provider you've enabled. No email/password accounts, no spam-bot signups, no anonymous posts. A commenter is a lightweight identity stored separately from both admin users and paid members.
- Auto avatars. The commenter's profile photo and display name come straight from the provider on each sign-in and are snapshotted onto every comment.
- Threaded replies. Comments support one level of nesting. A reply to a reply is automatically attached to the top-level comment so threads never run away.
- Three moderation policies. Go-live-without-approval, moderate-first-comment-only, or hold-everything (see below).
- Per-person controls. Ban a troublemaker (they can no longer sign in or comment, and their existing comments are hidden) or whitelist a trusted regular (their comments always auto-approve).
- Moderation queue. Dashboard → Comments, with Pending / Approved / Spam / Trash tabs, live counts, search, and a pending-count badge in the sidebar.
- Notifications. Optional immediate email when a comment needs review, plus an optional batched digest on an interval you choose.
How it works
-
Identity & guard. A commenter is an
App\Models\Commenterauthenticated through a dedicatedcommentersauth guard, completely separate from the adminwebguard and the optional Membershipsmembersguard. A signed-in commenter is never treated as an admin, and commenting works whether or not the Memberships add-on is installed. One person who signs in with Google and later with GitHub using the same email is linked to a single commenter via thecommenter_oauth_identitiestable. -
Social sign-in (Socialite). Built on Laravel Socialite. Google, GitHub, Facebook, and X are first-party drivers; Microsoft, Apple, and Discord are community drivers (the SocialiteProviders packages). The provider list is config-driven (
App\Support\Comments\ProviderRegistry) — only providers you've enabled and supplied credentials for show up as sign-in buttons. Credentials are pasted into Settings and injected into Socialite at request time, so nothing sensitive is committed toconfig/services.php. -
Moderation decision. When a comment is posted,
App\Support\Comments\CommentServicedecides its status: banned → rejected, whitelisted → approved, otherwise it follows the configured policy. The same service is the single source of truth for the public form and any dashboard action. -
Public rendering (cache-safe + cheap). Blog post pages are full-page cached (ResponseCache), and the comment UI is split so that the cache does almost all the work:
- The approved comment list + count are server-rendered straight into the cached page (
<x-comments-thread>). They're identical for every visitor, so they live in the cached HTML — which means they're crawlable (good for SEO) and cost zero queries or extra requests on a cache hit. The page's cache is busted automatically whenever a comment's approved visibility changes (approve / spam / trash / delete / edit), via a hook on theCommentmodel. A brand-new pending comment doesn't touch the cached list, so it doesn't bust anything. - Only the composer is per-visitor (
comments.composerLivewire island): the sign-in buttons vs. the compose box, the reply form, and the viewer's own still-pending comments. The cached page renders the guest default (sign-in buttons). A non-secretwpcms_commentercookie is set at login; the page's JS calls the composer to personalize only when that cookie is present. So guests fire zero extra requests — they read a fully static, cached page — while a signed-in visitor pays exactly one round-trip to load their compose box. One visitor's sign-in state can never leak into another's cached page.
- The approved comment list + count are server-rendered straight into the cached page (
-
Moderation queue + settings. Dashboard → Comments is the queue; Dashboard → Settings → Comments configures providers, policy, notifications, and the digest.
Moderation policies
Set under Dashboard → Settings → Comments → Approval policy:
- Go live without approval — every comment publishes immediately. Best for low-volume, trusted communities.
- Moderate first comment only (default) — a commenter's first comment is held for review; once you approve it, all their future comments auto-publish. The best balance for most blogs: it stops drive-by spam without making regulars wait every time.
- Hold all comments for approval — nothing appears publicly until a moderator approves it.
Two per-person overrides always win over the policy:
- Whitelist — the commenter's comments are always auto-approved (and any pending ones are approved immediately).
- Ban — the commenter can no longer sign in or post, and all of their existing comments are moved to Spam.
Both are available from the ⋯ menu on any comment in the moderation queue.
Setting up a provider
For each provider you want to offer:
- Create an OAuth app in the provider's developer console.
- Copy the Redirect / Callback URL shown in Settings → Comments for that provider (
/comments/auth/{provider}/callback) into the provider's app configuration. - Paste the Client ID and Client Secret back into Settings → Comments and toggle the provider on.
Notes per provider:
- Google / GitHub / Facebook / X — standard Client ID + Client Secret.
- Microsoft / Discord — standard Client ID + Client Secret (community drivers; already bundled).
- Apple — Sign in with Apple does not use a static secret. Provide the Services ID (as the Client ID), Team ID, Key ID, and the contents of the
.p8private key. WebProCMS generates Apple's required short-lived signed JWT secret for you on each request.
Only providers that are both enabled and fully configured render as buttons, so partially-set-up providers never appear to visitors.
Notifications & digest
Under Settings → Comments → Notifications:
- Immediate email — emails the configured recipients (or all admins if left blank) the moment a comment lands in the queue.
- Digest — a batched email of everything still pending, sent on an interval (hourly / every 6h / every 12h / daily). The digest runs through the CMS's LazyCron scheduler, so it works without a system crontab; toggling it on or off registers/deregisters the task immediately.
Where the data lives
commenters— the social identities (name, email, avatar, banned/whitelisted flags).commenter_oauth_identities— one row per linked provider account (unique per provider + provider id).comments— the comments themselves (post, author snapshot, body,statusof pending/approved/spam/trashed,parent_idfor replies).settings(comments.*keys) — moderation policy, provider credentials, notification + digest configuration.
Deleting a commenter keeps their comments (the author name/avatar are snapshotted onto each comment); deleting a post removes its comments.
Turning comments off
Toggle Enable comments on blog posts off in Settings → Comments to hide the comment section site-wide (the post pages render without it). Existing comments are preserved and reappear if you re-enable. Individual posts always show the section when comments are enabled; the row can be removed from a specific post in the page editor like any other row.
Limitations
- Threading is one level deep by design.
- The baked approved list shows up to the most recent ~500 comments per post without pagination; very large threads are truncated in the display.
- Because the approved list is baked into the cached page, a newly-posted comment a visitor makes shows in the composer area immediately (live, or "awaiting approval"); it appears in the main thread on the next page load once the page cache has been busted.
- A cancelled or failed sign-in simply returns the visitor to the signed-out state on the post.