Skip to content

feat(comments): reactions + Wilson 'Best' sort (Tier 1)#1540

Open
marcusbellamyshaw-cell wants to merge 3 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:feat/comment-reactions
Open

feat(comments): reactions + Wilson 'Best' sort (Tier 1)#1540
marcusbellamyshaw-cell wants to merge 3 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:feat/comment-reactions

Conversation

@marcusbellamyshaw-cell

@marcusbellamyshaw-cell marcusbellamyshaw-cell commented Jun 18, 2026

Copy link
Copy Markdown

What does this PR do?

Adds comment reactions (positive-only "like") and a Wilson score "Best" sort to the <Comments> component. This is Tier 1 of the best-in-class comments RFC, approved by @ascorbic.

  • New _emdash_comment_reactions table (migration 044) — deduped per voter via a salted IP hash (same privacy primitive as comment ip_hash)
  • Public, honeypot- and rate-limited endpoint at GET/POST /_emdash/api/comments/:collection/:contentId/reactions
  • Two opt-in props on <Comments>: reactions (like button + live counts) and sort="best" (Wilson lower-bound ranking for top-level comments, replies stay chronological)
  • Progressive enhancement: the inline script is emitted only when reactions is enabled — pages without reactions ship zero additional JS
  • Fully additive: new table, new route, new optional props with behavior-preserving defaults

Closes #1497 (Tier 1)

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Sonnet 4.6 (claude-sonnet-4-6)

Screenshots / test output

Before — comments today (chronological, no reactions):

comments-before

After<Comments reactions sort="best" />:

comments-after

Test output: 118 tests pass, 15 new (Wilson math, toggle/dedupe, counts, rate-limit, best-sort ordering).

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9e370e2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,012 lines across 16 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet cla: needed labels Jun 18, 2026
@marcusbellamyshaw-cell

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

marcusbellamyshaw-cell and others added 3 commits June 18, 2026 16:39
Tier 1 of the best-in-class comments RFC. Visitors can react (like) to
approved comments; reactions are stored first-party in a new
_emdash_comment_reactions table, deduped per voter by a salted IP hash,
and served via a public, honeypot- and rate-limited endpoint at
GET/POST /_emdash/api/comments/:collection/:contentId/reactions.

The <Comments> component gains two opt-in props: `reactions` (render a
like button + live counts) and `sort="best"` (Reddit-style Wilson
lower-bound ranking). Posting is progressively enhanced via a tiny inline
script emitted only when reactions are enabled. Additive and
backward-compatible: new table, new route, new optional props with
behavior-preserving defaults. Includes a changeset and tests.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8);
all code verified against the repository and the full test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gration

The reactions endpoint (GET/POST
/_emdash/api/comments/:collection/:contentId/reactions) needs an explicit
injectRoute entry in injectCoreRoutes — without it the path falls through to
the page handler. Verified end-to-end against the demo: toggle on/off and
aggregate counts return the expected JSON.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ucket limit

Adversarial self-review follow-ups on the reactions prototype:

- toggle() no longer does read-then-write. A concurrent double-toggle from the
  same voter could both see "no row" and have the second INSERT violate the
  unique index, surfacing as a 500. Now an idempotent INSERT ... ON CONFLICT
  DO NOTHING branches on whether a row was written, so it never throws.
- handleReactionToggle rejects reactions outside an allowlist (currently just
  "like", matching the shipped widget) so a caller can't spam arbitrary
  reaction strings and bloat a comment's count map.
- The public reactions route documents that the salted-IP voter hash collapses
  to a shared "unknown" bucket without a trusted IP (per-visitor dedup is the
  Tier 2 visitor-identity primitive), mirroring the comment ingest route.

Adds handler-level tests (happy path, unsupported reaction, non-approved and
missing comment, concurrent-toggle invariant). lint + typecheck clean; 82
comment tests pass.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant