Skip to content

Comments — Improvement & Growth Plan

Package: capell-app/comments · Kind: package · Tier: premium · Product group: Capell Engagement · Bundle: comments · Status: Draft

Comments adds moderated, threaded, dynamically-loaded discussion to registered Capell content (pages and, when Blog is installed, articles). It exposes two surfaces: an admin moderation experience (CommentModerationInbox page, CommentResource + CommentAuthorResource, CommentStatsWidget, LatestCommentsWidget) and a frontend Livewire component (CommentThreadComponent) loaded post-page via a no-store endpoint (RenderCommentThreadController → route capell-comments.thread). Key Actions are CreateCommentAction, BuildPublicThreadAction / ResolvePublicCommentableThreadAction, TransitionCommentStatusAction, ToggleCommentReactionAction, RequestCommentEmailVerificationAction / VerifyCommentAuthorEmailAction, RequestCommentReplyNotificationAction, and RegisterDefaultCommentablesAction. Models/tables: comments, comment_authors, comment_tokens, comment_moderation_events, comment_reactions (all registered as protected tables, all soft-delete on comments). Deps: capell-app/{core,admin,frontend}, Filament, Livewire, lorisleiva/laravel-actions, spatie/laravel-data, spatie/laravel-settings. Author name/email are encrypted casts; email is also stored as an HMAC email_hash for lookup.

Current marketplace summary: “Add moderated, threaded discussion to any Capell page or article — with cache-safe public rendering, encrypted author records, and per-site moderation controls, no custom code required.” Marketplace media declared in the manifest: 5 committed assets: the extension card plus four route-backed PNG captures for the same surfaces listed in docs/screenshots.json (moderation inbox, comments resource, authors resource, public thread). The committed runtime captures were generated from a Capell screenshot-runner fixture with real Filament admin chrome and seeded public thread data, and the screenshot contract requires the current capell-app/block-library package name instead of the stale capell-app/content-blocks name.

  • 2026-06-03: Rewrote marketplace/Composer copy, declared LatestCommentsWidget in manifest contributions, and added manifest coverage for registered dashboard widgets.
  • 2026-06-04: Added public comment form bot-trap controls: a hidden honeypot field, configurable minimum form age, action-level rejection before persistence, component state reset, and action/Livewire tests.
  • 2026-06-04: Implemented real CommentsHealthCheck diagnostics for required storage tables, settings registration, the public thread route, and the public thread Livewire component, with focused failure-mode tests.
  • 2026-06-04: Implemented automatic local spam scoring for configured link-count and blocked-term rules, storing spam_reasons, marking flagged submissions as Spam, and skipping verification tokens for auto-spam comments.
  • 2026-06-04: Hardened the public submit throttle key to use commentable + IP data without attacker-controlled author email, with Livewire regression coverage.
  • 2026-06-04: Wired queued moderator notifications for configured moderator email addresses when new comments enter pending approval or pending email verification, with listener registration and status-gating tests.
  • 2026-06-04: Added auto-inject anonymous-leakage coverage for the real render-hook path plus package Arch tests for strict equality, public-runtime admin/authoring isolation, and public Blade database/authoring-marker guards.
  • 2026-06-04: Implemented settings-aware reply pagination with per-parent “load more replies” support, bounded child hydration, recursive public rendering, and Livewire-safe public comment DTO serialization.
  • 2026-06-04: Hardened public body sanitization by stripping invisible Unicode format controls and ASCII control bytes, and broadened spam link detection to count scheme, www., and bare-domain links without counting email domains.
  • 2026-06-04: Memoized the resolved public commentable model within each Livewire request, avoiding duplicate page lookups during submit/refresh flows without serializing Eloquent models into Livewire state.
  • 2026-06-04: Locale-pinned public timestamp labels to the resolved commentable language so diffForHumans() output follows page language_id instead of the ambient app locale, with Livewire serialization coverage.
  • 2026-06-04: Added enforceable performance-budget coverage for public thread hydration/rendering and admin comment widgets, and batched sibling reply counts to avoid empty-grandchild query fanout.
  • 2026-06-04: Added privacy retention and erasure tooling for old visitor hashes, moderation notes, tokens, and author PII, plus hash-secret rotation documentation.
  • 2026-06-04: Added the pluggable CommentSpamProvider contract, default local provider, configured provider chain, and spam-check context data for Akismet/Turnstile-style adapters.
  • 2026-06-04: Added public Like reactions with aggregate public counts, plus approved-reply notifications with tokenized parent-author opt-out handling.
  • 2026-06-04: Added committed illustrated marketplace gallery assets for all four required screenshot-contract surfaces, with manifest coverage to keep those gallery assets present.
  • 2026-06-06: Replaced the illustrated Comments marketplace previews with route-backed PNG captures from the Capell screenshot runner for the moderation inbox, comments resource, author resource, and public thread.
  • Shipped 2026-06-04: Wire moderator new-comment notificationsCommentCreated now has a queued NotifyModeratorsOfNewComment listener registered by CommentsServiceProvider. It notifies valid, de-duplicated addresses from config('capell-comments.notifications.moderators') when a comment lands in PendingApproval or PendingEmailVerification, and skips spam/approved comments. — src/Events/CommentCreated.php, src/Listeners/NotifyModeratorsOfNewComment.php, src/Notifications/ModerateCommentNotification.php, src/Providers/CommentsServiceProvider.php, tests/Feature/ModeratorNotificationTest.php — M

  • Shipped 2026-06-04: Make CommentsHealthCheck a real checkCommentsHealthCheck::runDiagnostics() now reports storage-table, settings-registration, thread-route, and Livewire-component checks, and passed() reflects the aggregate result. Focused tests cover happy path plus missing table, settings, and route failures. — src/Health/CommentsHealthCheck.php, tests/Feature/CommentsHealthCheckTest.php — M

  • Shipped 2026-06-04: Add a honeypot + minimum-render-age check to the public formthread.blade.php now renders a hidden honeypot field, CommentThreadComponent tracks formRenderedAt, and CreateCommentAction rejects honeypot-filled or too-fast submissions before persistence. The minimum age is configurable at capell-comments.spam.minimum_form_age_seconds. — src/Livewire/CommentThreadComponent.php, resources/views/livewire/thread.blade.php, src/Data/CreateCommentData.php — S

  • Shipped 2026-06-04: Paginate / lazy-load repliesBuildPublicThreadAction now resolves root and reply page sizes through CommentSettingsResolver, hydrates each parent with a bounded child page, exposes replyCount / hasMoreReplies, and accepts per-public-id reply limits. CommentThreadComponent tracks per-parent reply limits and exposes loadMoreReplies(), while the public Blade now renders nested comments recursively with a translated load-more action. — src/Actions/BuildPublicThreadAction.php, src/Livewire/CommentThreadComponent.php, src/Data/PublicCommentData.php, resources/views/livewire/partials/comment-list.blade.php, tests/Integration/Actions/PublicThreadActionTest.php, tests/Integration/CommentThreadComponentTest.php — M

  • Shipped 2026-06-04: Surface validation errors per-field on submitCommentThreadComponent::submit() now resets stale submit/error state, catches ValidationException from rate-limit and CreateCommentAction validation, and maps each message into Livewire’s component error bag so the existing per-field @error blocks bind reliably. Focused Livewire coverage proves guest name, email, and body validation messages surface on the matching fields and stale success/errors are replaced by the current submit result. — src/Livewire/CommentThreadComponent.php, tests/Integration/CommentThreadComponentTest.php — S

  • Shipped 2026-06-04: Stop double-querying the commentable on every Livewire actionCommentThreadComponent now keeps the decrypted/resolved model in private request-local properties and resets that cache on Livewire hydration. Submit flows reuse the same model for CreateCommentAction and the post-submit refreshComments() call, with query-count coverage proving the duplicate page lookup is gone. — src/Livewire/CommentThreadComponent.php, tests/Integration/CommentThreadComponentTest.php — S

  • Shipped 2026-06-04: Enforce public/admin performance budgetsBuildPublicThreadAction now batches approved child-count lookups for each sibling group and skips empty child fetches, removing the old per-comment empty-grandchild query fanout. PerformanceBudgetTest seeds a bounded hot public thread, asserts hydration stays at or below 20 queries, and checks warmed Blade rendering against capell.json frontendRenderBudgetMs: 20; CommentsAdminSurfaceTest asserts the stats/latest-comments widgets stay under adminQueryBudget: 40. — src/Actions/BuildPublicThreadAction.php, tests/Integration/PerformanceBudgetTest.php, tests/Integration/Filament/CommentsAdminSurfaceTest.php — M

  • Shipped 2026-06-03: Index/labels parity in adminLatestCommentsWidget is registered at runtime and declared in capell.json contributes[], with manifest coverage proving every dashboard widget stays discoverable by marketplace and Diagnostics tooling. — capell.json, tests/Unit/ManifestRequirementsTest.php — S

Manifest capabilities[] = comments, comments-admin, comments-frontend, comments-created-event — these are structurally present. The gaps below are category table-stakes that are advertised in config/README but not built, plus differentiators.

  • Shipped 2026-06-04: Spam scoring (table-stakes, advertised). ScoreCommentSpamAction reads config('capell-comments.spam.max_links') and spam.blocked_terms, returns CommentSpamScoreData, and CreateCommentAction stores spam_reasons, sets CommentStatus::Spam, stamps marked_spam_at, and skips verification-token creation for flagged submissions. Focused coverage proves direct scoring and create-flow persistence. — src/Actions/ScoreCommentSpamAction.php, src/Data/CommentSpamScoreData.php, src/Actions/CreateCommentAction.php, tests/Integration/Actions/CreateCommentActionTest.php

  • Shipped 2026-06-04: Rate-limit hardening (anti-abuse). The primary public submit throttle key now uses commentable type, commentable ID, and requester IP only, so changing $this->authorEmail cannot reset the primary bucket. Focused Livewire coverage proves a second submission to the same thread/IP is throttled even when the email changes. — src/Livewire/CommentThreadComponent.php, tests/Integration/CommentThreadComponentTest.php

  • Future upsell: moderator digests / richer templates. Instant configured-address moderator notifications are shipped. A daily moderation digest, role-based moderator discovery, and Email Studio-backed templates remain natural premium differentiators outside the current package roadmap; supports: capell-app/email-studio is the intended vehicle.

  • Shipped 2026-06-04: Reactions / voting (differentiator). comment_reactions stores Like reactions keyed by authenticated user or hashed visitor request data. ToggleCommentReactionAction only accepts approved comments on the current commentable, and PublicCommentData exposes aggregate reactionCount without leaking actor identities.

  • Shipped 2026-06-04: Author reply notifications (differentiator). RequestCommentReplyNotificationAction runs when a reply becomes approved via trusted-author auto-publish or moderation transition, sends a queued parent-author email only for verified non-blocked authors, and creates a ReplyNotificationOptOut token consumed by DisableCommentAuthorReplyNotificationsAction.

  • Future upsell: edit / delete window for authors. Authenticated/verified authors cannot edit or soft-delete their own comments from the frontend; only admins transition status. A short author edit window is useful product depth, but it is intentionally outside the completed moderation/cache-safety roadmap.

  • Shipped 2026-06-04: Akismet / external spam provider hook (differentiator). CommentSpamProvider lets host apps append Akismet, Turnstile, CAPTCHA, or reputation-service adapters through capell-comments.spam.providers. ConfiguredCommentSpamProvider combines provider scores, LocalCommentSpamProvider preserves built-in link/blocked-term checks, and CreateCommentAction passes sanitized body, site/commentable context, public author fields, IP/user-agent, and authenticated user context into the scorer.

  • Health check coverage shipped. src/Health/CommentsHealthCheck.php now exposes real Diagnostics results for the critical package-health declaration in capell.json. Residual risk: these checks cover package wiring, not spam scoring, notifications, or public render budgets.

  • Local spam scoring shipped. spam.max_links, spam.blocked_terms, the spam_reasons column, and linkCount() are now enforced during CreateCommentAction. Link detection now covers http(s)://, www., and bare-domain links while avoiding email-domain false positives. Residual risk: the heuristic remains intentionally local and simple; external spam providers, CAPTCHA/Turnstile, and richer reputation scoring remain separate roadmap items.

  • Throttle bypass via email field shipped. The public submit throttle no longer includes attacker-controlled author email in the primary bucket. Residual risk: there is not yet a secondary per-author-email cap or broader abuse telemetry.

  • Performance budgets shipped. Manifest budgets are now enforced by focused tests: a seeded public thread checks bounded hydration query count plus warmed Blade render time against frontendRenderBudgetMs: 20, and admin stats/latest-comments widget queries are checked against adminQueryBudget: 40. Residual risk: these are deterministic package-level guards, not production load tests across every host theme.

  • XSS / sanitization hardening shipped. Body is strip_tags + whitespace-collapsed, capped at 5000 chars, and now strips Unicode format controls plus ASCII control bytes before storage (src/Support/CommentBodySanitizer.php). Output is Blade-escaped via {{ }} and PublicCommentData omits sensitive fields, so stored-XSS and text-spoofing surface is low. Focused coverage proves bidi/zero-width stripping and broader link counting.

  • Public-output safety coverage shipped. BuildPublicThreadAction::toData() correctly emits only public id, sanitized body, display name, timestamp, depth, reply count, children — no status, model id, email, hashes, tokens, or admin URL (matches the README Public Safety contract). The thread endpoint sets Cache-Control: no-store, private (src/Http/Controllers/RenderCommentThreadController.php:20). AutoInjectRenderHookTest now exercises the real RenderHookRegistry auto-inject path and proves the cached shell omits comment bodies, author PII, model identifiers, moderation state, Livewire snapshots, and admin URLs. CommentsBoundaryTest adds package Arch coverage plus public Blade guards against database access and authoring markers.

  • PII / retention shipped. Author name/email are encrypted at rest and email_hash is HMAC. capell-comments:privacy-retention now deletes old/consumed tokens, clears stale visitor hashes and moderation notes, and can anonymize matching author records by email while preserving public comment bodies and thread structure. docs/privacy-and-retention.md documents package hash secrets, app.key fallback risk, rotation expectations, dry-run usage, and subject erasure behavior. Residual risk: plaintext internal_notes / moderation_note remain private admin fields until retention or erasure clears them; they are not encrypted separately.

  • Test gaps. Coverage now includes health diagnostics, spam scoring, sanitizer hardening, moderator notification wiring/status-gating, auto_inject anonymous-leakage, reply pagination, performance budgets, privacy retention/erasure, public-output Architecture guards, Livewire commentable memoization, locale-pinned public timestamps, public submit throttling, and bot-trap rejection at action and Livewire levels.

  • i18n shipped. Strings are translated via capell-comments:: namespaces ✔. Public timestamp labels are now produced by BuildPublicThreadAction with the commentable language locale, serialized through PublicCommentData, and rendered as preformatted labels in Blade so Livewire hydration does not fall back to the ambient app locale.

  • Done/Shipped: manifest pairing matches README. README “Best Used With” lists Blog, Email Studio, and HTML Cache; capell.json now declares all three under dependencies.supports.

Critique. The marketplace summary and the composer description are now more specific about cache-safe public rendering, encrypted author records, and per-site moderation controls. The marketplace gallery now uses committed route-backed PNG captures for the four high-value screenshot-contract surfaces in docs/screenshots.json: the moderation inbox, comments resource, author resource, and public thread.

Improved 1-sentence summary: “Add moderated, threaded discussion to any Capell page or article — with cache-safe public rendering, encrypted author records, and per-site moderation controls, no custom code required.”

Improved 3–4 sentence description: “Comments lets site owners open public, threaded discussion on pages and Blog articles while editors keep full control from a dedicated moderation inbox with author records, status transitions, and dashboard widgets. Threads load after the page via a private, no-store endpoint, so they never contaminate cached HTML or leak moderation state to anonymous visitors. Author identities are encrypted at rest and matched by HMAC, with optional email verification, guest-vs-authenticated identity modes, and configurable approval, throttling, and depth limits per site or content type. Developers register a commentable type once and the package resolves public-safe thread data for the frontend.”

Future media upsell: a short GIF of the moderation approve/reject flow and a before/after of a cached page with comments loading in would improve the listing, but the required marketplace screenshot contract is complete. The comments package contract is runner-readable, the marketplace gallery points at committed PNG captures, and its dependency list uses the current capell-app/block-library package name.

Pricing / tier / bundle positioning: tier: premium, bundle: comments, proposedLicense: paid, requestedCertification: first-party, supportPolicy: priority. The package now has the baseline premium moderation loop: spam scoring plus instant configured moderator notifications. Cross-sell paths already in deps/manifest: Blog (supports) for article discussion — lead with this in the listing; Email Studio (supports) for richer moderator/author notification templates and digests; HTML Cache (README “Best Used With”) — position the no-store design as the reason these two coexist safely. Extension-suite angle: package with Blog + Email Studio as an “Engagement Suite.”

Differentiators / value props: cache-safe post-load rendering; encrypted PII + HMAC dedup; per-site & per-commentable-type setting overrides; verification flows (verify-then-moderate / moderate-then-verify); soft-delete + full moderation audit trail (comment_moderation_events). Target buyer: content/marketing teams on Capell (esp. Blog users) who want managed discussion without standing up Disqus/a third party and without breaking page caching.

Keywords/tags (8–12): comments, discussion, threaded-comments, moderation, engagement, blog-comments, spam-filtering, email-verification, livewire, cache-safe, filament, capell.

ItemBucketEffortImpactSection ref
Shipped 2026-06-04: implement real CommentsHealthCheck (tables, settings, route, component)DoneMHigh§2, §4
Shipped 2026-06-04: implement automatic spam scoring (max_links, blocked_terms, write spam_reasons)DoneMHigh§3, §4
Shipped 2026-06-04: fix throttle key (drop attacker-controlled email from primary bucket)DoneSHigh§3, §4
Shipped 2026-06-04: wire moderator new-comment notification listener on CommentCreatedDoneMHigh§2, §3
Shipped 2026-06-04: add auto_inject anonymous-leakage tests and Arch testsDoneMHigh§4
Shipped 2026-06-03: add LatestCommentsWidget to capell.json contributes[]DoneSMed§2, §4
Shipped 2026-06-06: generate/wire the 4 runtime PNG captures from docs/screenshots.json; marketplace gallery now promotes Capell runner PNGs for the moderation inbox, comments resource, author resource, and public threadDoneSMed§5
Shipped 2026-06-04: implement reply pagination using reply_page_size; cap subtree queryDoneMHigh§2, §3
Shipped 2026-06-04: benchmark + assert frontendRenderBudgetMs/adminQueryBudgetDoneMMed§4
Shipped 2026-06-04: harden sanitizer (bidi/zero-width strip; broaden link detection)DoneSMed§4
Shipped 2026-06-04: memoize resolved commentable in Livewire componentDoneSLow§2
Shipped 2026-06-04: locale-pin public timestamps to page language_idDoneSLow§4
Shipped 2026-06-04: pluggable external spam provider (Akismet/Turnstile) contractDoneMMed§3
Shipped 2026-06-04: reactions/voting + author-reply notifications (Engagement Suite up-sell)DoneLMed§3, §5
Shipped 2026-06-04: PII retention/erasure command + secret-rotation docs for hashesDoneMMed§4