Comments System v2: Design Refresh, Spam Protection, HTMX & Reactions #43

Closed
opened 2026-03-03 22:24:20 +00:00 by mark · 1 comment
Owner

Current State

The comment system is a functional MVP with:

  • Model: Comment with parent/reply (one level deep), is_approved gating, IP tracking, 2000-char body limit
  • Spam protection: Honeypot field + IP-based rate limiting (3/min) — minimal
  • Moderation: All comments require manual approval via Wagtail admin (bulk approve action available)
  • Submit flow: Full page reload via standard POST to /comments/post/, redirect back with ?commented=1
  • Styling: Functional Tailwind classes but not leveraging the site's design language (neon accents, solid shadows, grid patterns, gradient strips, grayscale→colour transitions)
  • No interactivity: No likes/reactions, no live updates, no inline submission feedback

Proposed Improvements

1. Design Refresh — Align with Design Language

The comment section currently uses plain surface/border styling that feels disconnected from the rest of the site. Changes to bring it in line:

  • Comment cards: Add shadow-solid-dark / shadow-solid-light offset shadows on hover (matches article cards and featured sections)
  • Avatar squares: The gradient squares (from-brand-cyan to-brand-pink) are good — consider making them slightly larger and adding a subtle shadow-neon-cyan glow
  • Author names: Use text-gradient utility for author names or a hover:text-brand-cyan transition
  • Timestamps: Already font-mono text-xs which is correct
  • Section header: Add a cyan→pink gradient strip above the "Comments" heading (matching the featured article sections)
  • Form focus states: Currently focus:border-brand-pink — add focus:shadow-neon-pink for the glow effect used elsewhere
  • Submit button: Already uses hover:shadow-solid-* which is correct; ensure it matches the CTA style from hero sections
  • Empty state: The "No comments yet" message could use a more engaging treatment — perhaps a subtle bg-grid-pattern background with centered text
  • Reply indentation: The ml-8 left indent is fine but consider adding a left border accent in brand-cyan (border-l-2 border-brand-cyan) instead of/alongside the indent
  • Transitions: Add transition-all duration-300 on comment cards for hover effects (scale, shadow, border-colour shift — like article cards)

2. Spam Protection — Replace Manual Approval with Cloudflare Turnstile

Problem: Every comment requires manual approval. This doesn't scale and means legitimate comments sit in limbo.

Solution: Cloudflare Turnstile — a free, invisible CAPTCHA alternative.

Why Turnstile?

  • Free — 1M requests/month on the free tier (more than enough)
  • Invisible — most users never see a challenge; no puzzle-solving
  • Privacy-respecting — no Google tracking, GDPR/CCPA compliant
  • No Cloudflare hosting required — works standalone with any site
  • WCAG 2.1 AA accessible

Implementation approach:

  1. Register site at dash.cloudflare.com → Turnstile → Add Widget
  2. Add the Turnstile JS widget to comment forms (<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>)
  3. Add <div class="cf-turnstile" data-sitekey="..."></div> inside each comment form
  4. Server-side: validate the cf-turnstile-response token via POST to https://challenges.cloudflare.com/turnstile/v0/siteverify in CommentCreateView.post() before saving
  5. Store TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY in environment/settings
  6. Auto-approve comments that pass Turnstile — change is_approved default to True when Turnstile validates, keep False as fallback if Turnstile is not configured
  7. Keep the honeypot field as a secondary layer
  8. Keep the rate limiter as a tertiary layer

Moderation change: With Turnstile, shift from "approve everything" to "flag and review reported/suspicious comments". The existing Wagtail admin bulk actions can be repurposed for un-approving/removing flagged content.

3. HTMX Integration — No Page Reload, Live Updates

Problem: Comment submission triggers a full page reload. No way to see new comments without refreshing.

Implementation:

  1. Add django-htmx to requirements and middleware
  2. Include HTMX via CDN (htmx.org@2.x) in the base template
  3. Extract comment list and individual comment into partials:
    • templates/comments/_comment_list.html — the full approved comments list
    • templates/comments/_comment.html — single comment (with replies)
    • templates/comments/_comment_form.html — the post form
    • templates/comments/_reply_form.html — inline reply form
  4. Add HTMX attributes to the comment form:
    <form hx-post="{% url 'comment_post' %}" 
          hx-target="#comments-list" 
          hx-swap="beforeend" 
          hx-on::after-request="this.reset()">
    
  5. Update CommentCreateView to detect request.htmx and return the partial instead of redirecting
  6. Add success feedback inline (e.g. a toast/banner: "Comment posted!" or "Comment awaiting moderation")
  7. Polling for new comments: Add hx-get with hx-trigger="every 30s" on the comments container to fetch new comments periodically
  8. Progressive enhancement: if JS is disabled, the standard POST/redirect still works

4. Comment Reactions (Likes/Hearts)

New feature: Allow users to react to comments without needing to post a reply.

Model:

class CommentReaction(models.Model):
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name="reactions")
    reaction_type = models.CharField(max_length=20, choices=[("heart", "❤️"), ("plus_one", "👍")])
    session_key = models.CharField(max_length=64)  # anonymous session-based dedup
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ["comment", "reaction_type", "session_key"]

Implementation:

  • HTMX-powered: hx-post="/comments/{id}/react/" with hx-swap="outerHTML" to update the reaction count inline
  • Session-based (not auth-based) — visitors can react once per type per comment
  • Display reaction counts on each comment with a small heart/thumbs-up icon
  • Style: small font-mono text-xs count next to a text-brand-pink heart icon, with hover:scale-110 transition-transform
  • Rate limit reactions same as comments (IP-based)

Implementation Order

  1. HTMX integration — foundational, enables everything else to be interactive
  2. Design refresh — restyle with design language; extract comment partials (needed for HTMX anyway)
  3. Cloudflare Turnstile — replace manual approval with invisible bot protection
  4. Reactions — add like/heart functionality

Technical Notes

  • Current deps: Django 5.2, Wagtail 7.0, Tailwind 3.4, django-tailwind, django-csp
  • New deps needed: django-htmx
  • CSP headers (django-csp) will need updating to allow Turnstile and HTMX script sources
  • Existing tests in apps/comments/ will need updating for HTMX partial responses
  • The data-comment-form attribute on the form is already there — likely intended for future JS use
  • The PII purge management command should be extended to cover CommentReaction.session_key
## Current State The comment system is a functional MVP with: - **Model**: `Comment` with parent/reply (one level deep), `is_approved` gating, IP tracking, 2000-char body limit - **Spam protection**: Honeypot field + IP-based rate limiting (3/min) — minimal - **Moderation**: All comments require manual approval via Wagtail admin (bulk approve action available) - **Submit flow**: Full page reload via standard POST to `/comments/post/`, redirect back with `?commented=1` - **Styling**: Functional Tailwind classes but not leveraging the site's design language (neon accents, solid shadows, grid patterns, gradient strips, grayscale→colour transitions) - **No interactivity**: No likes/reactions, no live updates, no inline submission feedback --- ## Proposed Improvements ### 1. Design Refresh — Align with Design Language The comment section currently uses plain surface/border styling that feels disconnected from the rest of the site. Changes to bring it in line: - **Comment cards**: Add `shadow-solid-dark` / `shadow-solid-light` offset shadows on hover (matches article cards and featured sections) - **Avatar squares**: The gradient squares (`from-brand-cyan to-brand-pink`) are good — consider making them slightly larger and adding a subtle `shadow-neon-cyan` glow - **Author names**: Use `text-gradient` utility for author names or a `hover:text-brand-cyan` transition - **Timestamps**: Already `font-mono text-xs` which is correct - **Section header**: Add a cyan→pink gradient strip above the "Comments" heading (matching the featured article sections) - **Form focus states**: Currently `focus:border-brand-pink` — add `focus:shadow-neon-pink` for the glow effect used elsewhere - **Submit button**: Already uses `hover:shadow-solid-*` which is correct; ensure it matches the CTA style from hero sections - **Empty state**: The "No comments yet" message could use a more engaging treatment — perhaps a subtle `bg-grid-pattern` background with centered text - **Reply indentation**: The `ml-8` left indent is fine but consider adding a left border accent in `brand-cyan` (`border-l-2 border-brand-cyan`) instead of/alongside the indent - **Transitions**: Add `transition-all duration-300` on comment cards for hover effects (scale, shadow, border-colour shift — like article cards) ### 2. Spam Protection — Replace Manual Approval with Cloudflare Turnstile **Problem**: Every comment requires manual approval. This doesn't scale and means legitimate comments sit in limbo. **Solution**: [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) — a free, invisible CAPTCHA alternative. **Why Turnstile?** - ✅ **Free** — 1M requests/month on the free tier (more than enough) - ✅ **Invisible** — most users never see a challenge; no puzzle-solving - ✅ **Privacy-respecting** — no Google tracking, GDPR/CCPA compliant - ✅ **No Cloudflare hosting required** — works standalone with any site - ✅ **WCAG 2.1 AA accessible** **Implementation approach:** 1. Register site at [dash.cloudflare.com](https://dash.cloudflare.com) → Turnstile → Add Widget 2. Add the Turnstile JS widget to comment forms (`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`) 3. Add `<div class="cf-turnstile" data-sitekey="..."></div>` inside each comment form 4. Server-side: validate the `cf-turnstile-response` token via POST to `https://challenges.cloudflare.com/turnstile/v0/siteverify` in `CommentCreateView.post()` before saving 5. Store `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` in environment/settings 6. **Auto-approve comments that pass Turnstile** — change `is_approved` default to `True` when Turnstile validates, keep `False` as fallback if Turnstile is not configured 7. Keep the honeypot field as a secondary layer 8. Keep the rate limiter as a tertiary layer **Moderation change**: With Turnstile, shift from "approve everything" to "flag and review reported/suspicious comments". The existing Wagtail admin bulk actions can be repurposed for un-approving/removing flagged content. ### 3. HTMX Integration — No Page Reload, Live Updates **Problem**: Comment submission triggers a full page reload. No way to see new comments without refreshing. **Implementation:** 1. Add `django-htmx` to requirements and middleware 2. Include HTMX via CDN (`htmx.org@2.x`) in the base template 3. Extract comment list and individual comment into partials: - `templates/comments/_comment_list.html` — the full approved comments list - `templates/comments/_comment.html` — single comment (with replies) - `templates/comments/_comment_form.html` — the post form - `templates/comments/_reply_form.html` — inline reply form 4. Add HTMX attributes to the comment form: ```html <form hx-post="{% url 'comment_post' %}" hx-target="#comments-list" hx-swap="beforeend" hx-on::after-request="this.reset()"> ``` 5. Update `CommentCreateView` to detect `request.htmx` and return the partial instead of redirecting 6. Add success feedback inline (e.g. a toast/banner: "Comment posted!" or "Comment awaiting moderation") 7. **Polling for new comments**: Add `hx-get` with `hx-trigger="every 30s"` on the comments container to fetch new comments periodically 8. Progressive enhancement: if JS is disabled, the standard POST/redirect still works ### 4. Comment Reactions (Likes/Hearts) **New feature**: Allow users to react to comments without needing to post a reply. **Model:** ```python class CommentReaction(models.Model): comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name="reactions") reaction_type = models.CharField(max_length=20, choices=[("heart", "❤️"), ("plus_one", "👍")]) session_key = models.CharField(max_length=64) # anonymous session-based dedup created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ["comment", "reaction_type", "session_key"] ``` **Implementation:** - HTMX-powered: `hx-post="/comments/{id}/react/"` with `hx-swap="outerHTML"` to update the reaction count inline - Session-based (not auth-based) — visitors can react once per type per comment - Display reaction counts on each comment with a small heart/thumbs-up icon - Style: small `font-mono text-xs` count next to a `text-brand-pink` heart icon, with `hover:scale-110 transition-transform` - Rate limit reactions same as comments (IP-based) --- ## Implementation Order 1. **HTMX integration** — foundational, enables everything else to be interactive 2. **Design refresh** — restyle with design language; extract comment partials (needed for HTMX anyway) 3. **Cloudflare Turnstile** — replace manual approval with invisible bot protection 4. **Reactions** — add like/heart functionality ## Technical Notes - Current deps: Django 5.2, Wagtail 7.0, Tailwind 3.4, django-tailwind, django-csp - New deps needed: `django-htmx` - CSP headers (`django-csp`) will need updating to allow Turnstile and HTMX script sources - Existing tests in `apps/comments/` will need updating for HTMX partial responses - The `data-comment-form` attribute on the form is already there — likely intended for future JS use - The PII purge management command should be extended to cover `CommentReaction.session_key`
Owner

Great direction overall. This is a solid v2 target, and I think it solves real MVP pain.

Main risks / gotchas I’d address before implementation:

  1. Auto-approval logic and fallback
  • I would not change the Comment.is_approved model default to True globally.
  • Keep model default False, then set comment.is_approved = turnstile_ok in the create view only when Turnstile is enabled and verification succeeds.
  • Fail closed on verification errors/timeouts (don’t auto-approve if Cloudflare is unavailable).
  • Verify more than success: also validate hostname (and action if used) to prevent token replay/misuse.
  1. CSP will currently block both HTMX CDN and Turnstile
  • Current CSP is strict: script-src 'self' 'nonce-...', connect-src 'self', and no frame-src allowlist.
  • Turnstile needs script/frame/connect permissions to Cloudflare; CDN HTMX also needs script allowlisting.
  • Prefer self-hosting HTMX from static files to minimize CSP surface area, then only allow Turnstile domains.
  • Add security tests for updated CSP behavior.
  1. HTMX response contract needs to be explicit
  • Proposed hx-target="#comments-list" + hx-swap="beforeend" can break if the server returns full list HTML or form errors.
  • I’d define strict response modes:
    • approved success -> return single comment partial for append
    • pending/moderated success -> return status/notice partial (no list append)
    • validation/spam failure -> return form partial with errors (422)
  • If the same endpoint serves both full-page and HTMX partials, add Vary: HX-Request.
  1. Polling strategy can create duplicates
  • every 30s polling + append behavior can duplicate comments.
  • Either:
    • poll for deltas (e.g., ?after_id=), or
    • replace the full list (outerHTML) instead of appending.
  • Add explicit ordering (order_by(created_at, id)) so UI order is deterministic.
  1. Reactions: important edge cases
  • Use UniqueConstraint (modern replacement for unique_together).
  • Ensure anonymous users always have a session key before dedupe.
  • Handle races (get_or_create + IntegrityError) and use atomic increments/F expressions for counts.
  • Use a separate rate-limit bucket from comment posting; reusing the current 3/min comment limit will feel broken.
  • For button-driven HTMX reactions, confirm CSRF token/header is sent.
  1. Moderation tooling gap
  • Proposal says “review flagged/suspicious comments,” but current Wagtail bulk action only supports Approve.
  • Add bulk actions for Unapprove and Delete (or soft-hide), otherwise moderation workflow will regress.
  1. Data/privacy alignment
  • Good catch on extending PII purge to reactions.
  • Include CommentReaction.session_key in retention/anonymization policy and tests.

Suggested sequence (slightly safer):

  1. Partial template extraction + response contracts + tests (no behavior change)
  2. Turnstile integration behind feature flag + conditional auto-approval
  3. HTMX progressive enhancement (submit + optional polling)
  4. Reactions
  5. Visual design refresh pass

Extra tests that will pay off quickly:

  • Turnstile success/failure/timeout + feature-disabled behavior
  • CSP allows required external origins and still blocks everything else
  • HTMX success/error flows + no-JS POST/redirect fallback
  • No duplicate comments under polling
  • Reaction dedupe/concurrency/rate-limit behavior
Great direction overall. This is a solid v2 target, and I think it solves real MVP pain. Main risks / gotchas I’d address before implementation: 1. Auto-approval logic and fallback - I would not change the `Comment.is_approved` model default to `True` globally. - Keep model default `False`, then set `comment.is_approved = turnstile_ok` in the create view only when Turnstile is enabled and verification succeeds. - Fail closed on verification errors/timeouts (don’t auto-approve if Cloudflare is unavailable). - Verify more than `success`: also validate `hostname` (and `action` if used) to prevent token replay/misuse. 2. CSP will currently block both HTMX CDN and Turnstile - Current CSP is strict: `script-src 'self' 'nonce-...'`, `connect-src 'self'`, and no `frame-src` allowlist. - Turnstile needs script/frame/connect permissions to Cloudflare; CDN HTMX also needs script allowlisting. - Prefer self-hosting HTMX from static files to minimize CSP surface area, then only allow Turnstile domains. - Add security tests for updated CSP behavior. 3. HTMX response contract needs to be explicit - Proposed `hx-target="#comments-list"` + `hx-swap="beforeend"` can break if the server returns full list HTML or form errors. - I’d define strict response modes: - approved success -> return single comment partial for append - pending/moderated success -> return status/notice partial (no list append) - validation/spam failure -> return form partial with errors (`422`) - If the same endpoint serves both full-page and HTMX partials, add `Vary: HX-Request`. 4. Polling strategy can create duplicates - `every 30s` polling + append behavior can duplicate comments. - Either: - poll for deltas (e.g., `?after_id=`), or - replace the full list (`outerHTML`) instead of appending. - Add explicit ordering (`order_by(created_at, id)`) so UI order is deterministic. 5. Reactions: important edge cases - Use `UniqueConstraint` (modern replacement for `unique_together`). - Ensure anonymous users always have a session key before dedupe. - Handle races (`get_or_create` + `IntegrityError`) and use atomic increments/F expressions for counts. - Use a separate rate-limit bucket from comment posting; reusing the current `3/min` comment limit will feel broken. - For button-driven HTMX reactions, confirm CSRF token/header is sent. 6. Moderation tooling gap - Proposal says “review flagged/suspicious comments,” but current Wagtail bulk action only supports `Approve`. - Add bulk actions for `Unapprove` and `Delete` (or soft-hide), otherwise moderation workflow will regress. 7. Data/privacy alignment - Good catch on extending PII purge to reactions. - Include `CommentReaction.session_key` in retention/anonymization policy and tests. Suggested sequence (slightly safer): 1) Partial template extraction + response contracts + tests (no behavior change) 2) Turnstile integration behind feature flag + conditional auto-approval 3) HTMX progressive enhancement (submit + optional polling) 4) Reactions 5) Visual design refresh pass Extra tests that will pay off quickly: - Turnstile success/failure/timeout + feature-disabled behavior - CSP allows required external origins and still blocks everything else - HTMX success/error flows + no-JS POST/redirect fallback - No duplicate comments under polling - Reaction dedupe/concurrency/rate-limit behavior
mark started working 2026-03-03 22:42:04 +00:00
mark closed this issue 2026-03-04 00:04:43 +00:00
mark worked for 1 hour 22 minutes 2026-03-04 00:04:43 +00:00
Sign in to join this conversation.
No Label
2 Participants
Notifications
Total Time Spent: 1 hour 22 minutes
mark
1 hour 22 minutes
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nohype/main-site#43