Why Comments?
Posts are not conversations. They publish, people read them, and most of the time nothing comes back at all.
On a good day, a reaction arrives through email or a reply on LinkedIn or Bluesky. That is useful, but invisible to everyone else. Someone asks a question worth answering, I answer it, and the next reader with the same question has to start over from nothing.
A comment section is the obvious fix. Comments keep the conversation on the page, visible to everyone who follows.
But comments are not free. They introduce moderation work, spam risk, and content on my site that I did not write and cannot always predict. Adding them only makes sense if the system handles those costs well enough for the benefit to be worth it.
There was already a comment system here when the site launched. The theme included an integration with Giscus, so enabling comments required very little site work.
It worked until it did not.
Comments failed to load on too many visits and sometimes did not appear after reloading the page. It was unreliable enough that removing it was better than leaving readers with something that only worked some of the time.
That made a proper evaluation worthwhile instead of simply reaching for the next easy option.
Requirements
This site is built with Astro in static output mode and deployed to Cloudflare Pages. There is no server-side runtime. Every page is pre-built HTML served from the edge.
Any comment system therefore needs to work without a Node.js handler at request time, an origin server attached to the site, or a persistent process that I need to keep running.
I initially wrote every desirable property as a hard requirement. The evaluation exposed that this was not entirely honest. Some things really were mandatory, while others were preferences that I was willing to trade against cost, usability, or operational effort.
The requirements were therefore divided into three groups.
Hard requirements
These determine whether a solution remains under consideration.
- Static-site compatibility β Must work as an external service, script embed, or web component on a static Astro site deployed to Cloudflare Pages.
- Login required β Anonymous commenting must be disabled.
- Third-party authentication β Commenters must be able to authenticate through an established identity provider rather than relying exclusively on a local username and password.
- Multiple OAuth providers β Must support more than GitHub alone. GitHub plus Google meets the minimum, while broader provider support is preferred.
- Threaded replies β Must support replies to individual comments rather than only top-level messages.
- Reply notifications β Commenters must be able to receive an email when someone replies to their comment.
- Moderator notifications β Must notify me when a new comment is posted.
- Proportionate DevOps cost β The combined cost of building, operating, and maintaining the solution must not exceed the value it delivers.
- Predictable service cost β Pricing must be foreseeable and remain within my control. A fixed monthly rate is preferable, but predictable usage-based billing is acceptable.
- Comment export β Must provide a way to export comment data for backup or future migration.
- Data deletion β Must provide a practical way to remove comments and personal data for an individual on request.
Evaluation criteria
These affect the decision but do not automatically disqualify a solution.
- Light asynchronous footprint β The embed should load asynchronously and not meaningfully degrade page performance.
- 2FA capability β Authentication should support or inherit two-factor authentication. With OAuth, this depends on whether the commenter has enabled 2FA with their identity provider.
- Built-in notifications β Native email notifications are preferable to webhooks that require additional integration.
- Script privacy β The embed should avoid advertising, cross-site behavioral tracking, and unnecessary cookies. Ideally, no third-party resources should load until the reader reaches or activates the comment section.
- GDPR support β The service should provide documented processes and controls that support GDPR obligations.
- Breadth of authentication providers β Supporting providers beyond GitHub and Google makes commenting accessible to more readers.
Preferences
These are desirable, but not worth satisfying at any cost.
- No vendor badge β No attribution badge in the embed by default, or a badge that can be removed at a proportionate price.
The distinction matters. If I am unwilling to pay substantially more solely to remove a vendor badge, then badge removal is not a hard requirement, regardless of how strongly I initially phrase it.
Alternatives
Pricing and feature availability reflect what was published at the time of writing. Both change. Verify against each solutionβs current documentation before deciding.
The candidates fall into three structurally different categories. This matters when comparing costs because a free open-source solution and a paid managed service exchange money and operational responsibility in very different ways.
- Managed services β Hosted by the vendor. You pay a subscription, and the vendor handles storage, availability, upgrades, and delivery. Candidates: Disqus, FastComments, Hyvor Talk, and CommentBox.io.
- Self-hosted software β Open-source software that is free to use, but infrastructure, upgrades, backups, monitoring, and delivery remain your responsibility. Candidates: Comentario, Remark42, and Garrul.
- Repository-backed systems β Comments are stored through GitHub Issues, Discussions, pull requests, or commits rather than in a dedicated comment service. Candidates: Giscus, Utterances, and Staticman.
A custom solution was also included as the baseline for maximum control.
Functional requirements
| Solution | Category | Static compatible | Async | Login | 2FA capable | Multi OAuth | Threaded | Reply notif. | Mod. notif. |
|---|---|---|---|---|---|---|---|---|---|
| Comentario | Self-hosted | β | β | β | β οΈ | β | β | β | β |
| CommentBox.io | Managed | β | β | β | β οΈ | β οΈ | β | β | β |
| Disqus | Managed | β | β | β | β οΈ | β | β | β | β |
| FastComments | Managed | β | β | β | β οΈ | β | β | β | β |
| Garrul | Self-hosted | β | β | β | β | β | β | β οΈ Webhook | β οΈ Webhook |
| Giscus | Repo-backed | β | β | β | β | β | β | Via GitHub | Via GitHub |
| Hyvor Talk | Managed | β | β | β | β | β | β | β | β |
| Remark42 | Self-hosted | β | β | β | β οΈ | β | β | β | β |
| Staticman | Repo-backed | β | β | β | β | β | β | β | β |
| Utterances | Repo-backed | β | β | β | β | β | β | Via GitHub | Via GitHub |
| Custom solution | Self-hosted | β | β | β | β | β | β | β | β |
Operational and privacy criteria
Managed-service pricing reflects the lowest tier considered suitable for this use case. For self-hosted software, a free license does not mean zero operating cost. Infrastructure, backups, updates, email delivery, and maintenance still have a cost.
| Solution | Category | DevOps cost | Service cost | No badge | Privacy | Export | GDPR | Notes |
|---|---|---|---|---|---|---|---|---|
| Comentario | Self-hosted | $$$ | $β$$ | β | β | β | β | Free OSS; requires hosting, database operations, backups, upgrades, and email delivery |
| CommentBox.io | Managed | $ | $10 | β οΈ | β οΈ | β | β οΈ | OAuth providers, branding controls, and cookie behavior require further verification |
| Disqus | Managed | $ | $11 | β | β | β | β οΈ | Approximately 100K pageviews per month; known for third-party tracking |
| FastComments | Managed | $ | $5.99 | β | β οΈ | β | β οΈ | Approximately 500K pageviews per month |
| Garrul | Self-hosted | $$ | $0β$ | β | β | β | β | Runs on Cloudflare Workers; data remains in your Cloudflare account |
| Giscus | Repo-backed | $ | $0 | β | β οΈ | β | β οΈ | Free OSS; GitHub Discussions backend; GitHub accounts only |
| Hyvor Talk | Managed | $ | β¬12 | β οΈ | β οΈ | β | β | Premium 100K tier; badge removal requires the Business tier |
| Remark42 | Self-hosted | $$$ | $β$$ | β | β | β | β | Free OSS; requires a persistent host and ongoing operations |
| Staticman | Repo-backed | $$$$ | $β$$ | β | β | β | β οΈ | Requires a separately hosted Node.js API and a rebuild workflow |
| Utterances | Repo-backed | $ | $0 | β | β οΈ | β | β οΈ | Free OSS; GitHub Issues backend; no threaded replies |
| Custom solution | Self-hosted | $$$$$ | $$$ | β | β | β | β | Maximum control, but full development and operational responsibility |
First elimination
The first pass removed the candidates that clearly failed a hard requirement or required disproportionate operational effort.
| Solution | Category | Reason for elimination | Notes |
|---|---|---|---|
| Utterances | Repo-backed | Multi OAuth, threaded replies | GitHub accounts only; no threaded replies |
| Staticman | Repo-backed | Login, OAuth, threading, notifications, DevOps cost | Compatible with a static frontend, but requires a separately operated Node.js API and rebuild workflow |
| Giscus | Repo-backed | Multi OAuth | GitHub accounts only; also unreliable in practice on this site |
| Disqus | Managed | Privacy | Third-party tracking remained too significant for the role comments play on this site |
| Remark42 | Self-hosted | DevOps cost | Capable, but requires a persistent host and ongoing maintenance |
| Comentario | Self-hosted | DevOps cost | A maintained and capable solution, but with the same infrastructure burden as other self-hosted services |
| Custom solution | Self-hosted | Development and DevOps cost | Maximum control, but unjustified for this scope |
| CommentBox.io | Managed | Too many unresolved criteria | Authentication providers, branding controls, privacy behaviour, and GDPR handling required more verification than the stronger alternatives |
That left three candidates for closer evaluation:
- FastComments
- Hyvor Talk
- Garrul
Shortlist
All three remained viable against the core requirements, although Garrul requires additional integration for notifications and some details for FastComments still needed verification.
| Solution | Category | Service cost | DevOps cost | Multi OAuth | 2FA capable | Reply notif. | Mod. notif. | Privacy | GDPR | Notes |
|---|---|---|---|---|---|---|---|---|---|---|
| FastComments | Managed | $5.99 | $ | β οΈ | β οΈ | β | β | β οΈ | β οΈ | Low-cost managed option; authentication and privacy details needed further verification |
| Garrul | Self-hosted | $0β$ | $$ | β | β | β οΈ Webhook | β οΈ Webhook | β | β | GitHub and Google OAuth; data remains in your Cloudflare account |
| Hyvor Talk | Managed | β¬12 | $ | β | β | β | β | β οΈ | β | Broad OAuth support, built-in notifications, moderation, and export |
The structural difference was clear.
Garrul provides the strongest data sovereignty. Its code and data run inside my own Cloudflare account, and the direct service cost can remain close to zero. The trade-off is that setup, configuration, notification delivery, upgrades, and troubleshooting remain my responsibility.
FastComments and Hyvor Talk take the opposite approach. They charge a subscription but remove nearly all operational responsibility.
Demo testing
Numbers and icons only go so far.
After narrowing the field to three candidates, I tested the live demos as an actual commenter would:
- Post a comment
- Reply to an existing comment
- Follow a threaded conversation
- Sign in through an external identity provider
- Review the moderation experience
- Check how naturally the embed fits into the site
The experience difference was noticeable.
Hyvor Talk had the most polished embed. The interface was clean, the threading was easy to follow, and the moderation dashboard felt coherent.
FastComments was functional and less expensive, but the commenting and moderation experience felt less refined in comparison. Since comments are part of the reader-facing site rather than an internal administrative tool, that difference mattered more than the relatively small subscription saving.
Garrul was workable, but clearly optimized for control and self-hosting rather than a polished, ready-made experience. Its webhook-based notifications also meant that capabilities available immediately in the managed services would require additional configuration and ownership.
On experience alone, Hyvor Talk led. FastComments was removed from the shortlist.
Final decision
That left Hyvor Talk and Garrul.
The question was no longer primarily about features. It was whether Garrulβs privacy and data-sovereignty advantages justified taking on another self-hosted system.
They did not.
Nobody is forced to comment, so the privacy implications are lower than they would be for a mandatory service or core product workflow. The comment system still needs to be handled responsibly, but it does not justify unlimited operational investment.
More importantly, I had no particular interest in learning or operating the underlying comment infrastructure. Even at a conservative estimate of my time, provisioning, configuring, integrating, and maintaining Garrul would quickly cost more than the Hyvor Talk subscription.
That is why Garrul was set aside for the same reason as Remark42 and Comentario. The technology is capable, but the operational responsibility is disproportionate to the value of the feature.
The evaluation also changed two of my original requirements.
I initially treated badge removal and zero pre-interaction contact with the comment provider as mandatory. In practice, I was not willing to pay for the Business tier solely to remove the badge, and a managed comment embed necessarily communicates with its provider when it loads.
That makes both evaluation criteria rather than absolute constraints.
Hyvor Talk best satisfies the requirements that remain genuinely mandatory:
- Static-site compatibility
- Authenticated commenting
- Multiple OAuth providers
- Threaded replies
- Built-in reply and moderator notifications
- Comment export
- GDPR support
- Predictable pricing
- Negligible operational overhead
Hyvor Talk is selected, but real usage will determine whether the trade-offs remain acceptable.
Getting it running
Comments are now live. You can see them at the bottom of this page.
The implementation covered six areas:
- Selecting a plan
- Configuring the service
- Integrating the embed
- Enabling reactions
- Adjusting the page structure
- Running Lighthouse against the local production build
There was no staging detour. The plan is to run the system for a while, see how the moderation and notification flows behave in practice, and update this post with the outcome.
Signup
I selected the Premium plan at the 100K-pageview tier.
Premium includes the capabilities that matter for this use case:
- Moderation
- OAuth authentication
- Webhooks
- Slack notifications
- Spam detection
- Styling customisation
- Comment export
The plan does not include badge removal. That requires the Business tier.
I signed up knowing this and accepted the badge. The article already names Hyvor Talk as the selected solution, so attribution is not a problem. Paying for a substantially higher plan only to hide a small logo would not be a proportionate use of money.
That confirmed that badge removal was a preference rather than a real hard requirement.
Configuration
The first step after signup was creating a site in the Hyvor Talk dashboard. The site receives a name and the production URL where the comment system will be used.
This generates the website ID used by the embed and associates all comments with the correct site.
The next step was disabling guest commenting.
Hyvor Talk allows guest submissions by default. That conflicts with the requirement that every commenter authenticate through an external identity provider. A setting in the dashboard disables guest submissions and requires login before commenting.
The third step was configuring the moderator profile, including the display name, profile image, and account details. This is the identity shown when I respond as the site owner.
To load and test the embed during development, localhost also had to be added to the list of allowed domains. Without it, the embed did not appear locally.
Installing it on the site
The integration was as straightforward as the documentation suggested.
Hyvor Talk is distributed as a web component:
- A
<script>element registers the component. - A
<hyvor-talk-comments>element places the comment interface on the page.
I wrapped both in a small Astro component and passed post.id as the page-id.
Using a stable post identifier rather than the current URL means comments remain associated with the article even if its URL structure changes later.
The component was then added to the blog-post layout.
No Astro adapter, server-side handler, additional build process, or configuration file was required. From the final decision to a working local embed took less than ten minutes.
Reactions
Hyvor Talk also includes a reaction system β emoji responses to the post itself. It was not part of the evaluation at all, but it comes included with the plan.
Unlike comments, reactions are left open to anonymous use. Requiring login for a quick emoji response would add unnecessary friction for something that carries no content and creates no moderation burden. Comments enforce login because what gets written matters. Reactions are lightweight signals and can stay open.
Page structure
The original article layout had two callouts below each post:
- A call to contact me
- A prompt to follow the RSS feed
With comments now available, a dedicated contact callout made less sense. The comment section already provides the most direct place to discuss the article.
The two callouts were replaced with a single engagement callout:
- Leave a public comment
- Use another contact channel for a private conversation
- Follow the RSS feed for future updates
The Callout component also received a new engage type using the siteβs accent blue instead of the blue and purple gradients used by the previous cta and rss variants.
The result is one callout, using the right visual treatment, positioned between the article and its comments.
Lighthouse: local test
With the embed running locally, I ran Lighthouse against the local production build to establish a baseline before deploying it.
The mobile scores under simulated slow 4G were:
| Category | Score |
|---|---|
| Performance | 99 |
| Accessibility | 93 |
| Best practices | 96 |
| SEO | 100 |
Performance was therefore not a concern.
Lighthouse identified the Hyvor Talk script as a third-party dependency, which is expected. This confirms that loading the embed contacts Hyvor Talk before a reader actively submits a comment. It does not meet the strictest interpretation of my original script-privacy requirement, but it remains an accepted trade-off unless production testing reveals tracking or cookie behavior that I consider disproportionate.
The two accessibility findings came from the embed.
The first concerned Hyvor Talkβs ProseMirror editor. The comment field uses role="textbox" without an accessible label, which means a screen reader may announce it with a generic name. This appears to be an issue in the embedded component rather than something exposed through site configuration.
The second finding concerned placeholder-text contrast. This may be possible to improve through Hyvor Talkβs theme configuration and is worth testing after deployment.
Best practices identified two additional items:
- A WebSocket connection to
soketi.hyvor.comreturned a 400 response during local testing. - Some recommended security headers were missing.
The WebSocket issue is likely related to the local development environment and needs to be checked again in production.
The security headers are a site-wide infrastructure concern, not specific to the comment system, but adding the embed did expose one gap.
None of the local findings blocked deployment.
Going live
Deploying surfaced what local testing could not. The problems fell into three groups: two that blocked the embed outright, a round of auditing and refinement once it worked, and two that came down to a trade-off between a convenience and a strict security policy.
Content Security Policy: the embed was blocked
After deploying, the embed failed to load in production. The browser console identified the cause: the siteβs Content Security Policy was blocking the Hyvor Talk script because talk.hyvor.com was not in the script-src allowlist.
The fix required three changes to public/_headers:
script-srcβ addedhttps://talk.hyvor.comto allow the embed script to loadconnect-srcβ addedhttps://talk.hyvor.comandwss://soketi.hyvor.comfor API calls and the realtime WebSocketimg-srcβ broadened tohttps:to allow OAuth provider avatars from Google, GitHub, Twitter/X, Apple, and others
This was a straightforward change once the console error identified the blocked resource. The CSP infrastructure pre-existed; it just needed updating to account for the new third-party dependency.
Login: Cross-Origin-Opener-Policy fix
With the embed loading, the next issue appeared on login. The OAuth popup opened and the authentication succeeded, but the popup could not post the result back to the parent page. The browser console reported:
Uncaught TypeError: Cannot read properties of null (reading 'postMessage')
The cause was Cross-Origin-Opener-Policy: same-origin. This header severs the window.opener reference when a cross-origin popup is opened β which is exactly what the OAuth login flow does. The popup opens on a different origin, completes authentication, and then calls window.opener.postMessage() to hand the token back to the parent. With same-origin, window.opener is null and the call fails.
The fix was to change the header to same-origin-allow-popups. This preserves COOP protection for same-origin navigations while allowing popups to retain the opener reference they need to complete the login handoff.
Lighthouse: production audit
With the embed fully functional in production, I ran Lighthouse again. Accessibility had dropped from 93 to 90. All four failing audits originated inside the <hyvor-talk-comments> shadow DOM:
- The ProseMirror editorβs
role="textbox"still has no accessible label. - The placeholder text and comment action buttons (Reply, Edit, Delete) failed contrast thresholds in dark mode, where Hyvor Talk was using
#767676on#171717β a ratio of 3.94:1 against the 4.5:1 minimum. - Reaction buttons have an
aria-labelthat does not match the visible text, which includes the reaction count. - The notification and mod-icon buttons in the embed header are smaller than the 24 Γ 24 px touch target minimum.
The contrast was fixable through Hyvor Talkβs palette configuration. Setting the βBox Text lightβ value to #757575 in the light palette and #9a9a9a in the dark palette brought both above the 4.5:1 threshold. After that change, accessibility recovered to 93 β the same score as the local test.
The other issues require changes to the embed itself. A support ticket covering them was filed with Hyvor Talk. One more contrast case surfaced later: inline code inside a comment renders as #eb5757 on the dark comment background β 4.28:1, just under the threshold. That color is not exposed in the palette, so it went into the same ticket.
Theme sync
The embed was defaulting to its light palette regardless of the site theme. A small script was added to HyvorTalk.astro that reads the dark class on <html> at load time and sets the colors attribute on the embed accordingly. A MutationObserver keeps the two in sync when the reader switches theme using the footer buttons.
CSP: an inline script that cannot be pinned
Hyvor Talk injects an inline script into the parent document at runtime, which a strict script-src blocks. The usual fix is to whitelist the scriptβs SHA-256 hash. That does not work here: the scriptβs content is dynamic, so its hash changes from page to page and whenever comment content changes. Over a single afternoon I watched four different hashes go by. Chasing them is unwinnable.
The alternatives were worse. 'unsafe-inline' would unblock it, but only by disabling inline-script protection across the whole site β trading Hyvor Talkβs problem for a real weakness of my own. So I left the script blocked. The embed still works without it; the only cost is a Best Practices ding and some console noise. The strict policy stays intact, and there is nothing to maintain.
The correct fix belongs to Hyvor Talk: move the script into their hosted embed bundle, which is already whitelisted. That went into the support ticket too.
View Transitions: a strict CSP is incompatible
The site used Astroβs View Transitions for SPA-style navigation. On pages with the embed, navigating in showed a permanent spinner; only a hard reload rendered the comments. Two separate problems were tangled together.
The first was the embed itself. Its custom element does not re-initialize cleanly after Astro swaps the DOM β a stale reference to a style node it had already moved threw NotFoundError: Failed to execute 'removeChild', and initialization never completed.
The second was more fundamental. View Transitions re-executes page scripts by loading them from data: URIs, and a strict script-src blocks data:. Allowing it would have been another 'unsafe-inline' in disguise. View Transitions and a strict CSP simply cannot coexist without loosening the policy.
Rather than weaken the CSP for a navigation nicety, I removed View Transitions. Every navigation is now a full page load β which, on static HTML served from the edge, is already fast. The spinner disappeared with it, since there is no longer a DOM swap to leave the embed in a broken state.
Whatβs next
Hyvor Talk has more functionality than I configured initially:
- Moderation rules
- Spam controls
- Email templates
- Webhooks
- Slack notifications
- Styling options
- User management
- Import and export tools
There is no reason to configure everything before real usage reveals what matters.
The system starts with a minimal configuration. When an operational need appears, it can be evaluated and added. Features that never become relevant do not need to become part of the siteβs complexity.
The main things to verify in production are:
- Whether notifications work reliably end to end
- Whether spam becomes a practical problem
- Whether the embed sets unnecessary cookies
- Whether readers actually use the comment section
If Hyvor Talk earns its place, I will publish a follow-up with the results.
Tried a different approach to comments on a static site? Leave a comment below, or reach out through another channel if you would rather keep the conversation private. Curious how Hyvor Talk holds up in practice? Follow the RSS feed and I will publish an update.