EchoThread Comment Widget Documentation
Use these EchoThread docs to install a privacy-first comment widget on any website in under a minute. Start with the copy-paste embed snippet, then configure thread identifiers, theming, import/export, moderation, and platform setup for Shopify, Wix, Framer, Squarespace, Hostinger, WordPress, Next.js, Hugo, Astro, Ghost, Jekyll, Eleventy, and Gatsby.
Last updated · See the changelog for recent widget updates, or the EchoThread blog for in-depth guides.
Popular setup paths
- Add comments to a Shopify blog
- Add comments to Wix blog posts
- Add comments to Framer pages and CMS posts
- Add comments to Squarespace
- Add comments to an Astro blog
- Hugo comments setup guide
- Next.js comments setup guide
- Add comments to Ghost
- Replace the Facebook Comments plugin
- Best commenting system for static sites
- GDPR-compliant comment system
- Compare EchoThread with alternatives
Quick start
Get EchoThread running on your site in under a minute. No build tools required.
Create an account & register your site
Sign up for free, then add your website domain in the dashboard. You'll receive an API key for your site.
Add the embed snippet
Paste this before your closing </body> tag, replacing YOUR_API_KEY with the key from your dashboard:
<div id="echothread"
data-api-key="YOUR_API_KEY"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>You're live
Comments will appear on your page immediately. Manage them from the moderation dashboard.
Configuration
Customize the widget using data-* attributes on the container element.
| Attribute | Default | Description |
|---|---|---|
| data-api-key | (required) | Your site API key from the dashboard. |
| data-page-url | window.location.href | Canonical URL of the page. Used to group comments into threads. |
| data-identifier | page URL | Stable unique ID for the comment thread. Use a slug or database ID so threads survive URL changes. |
| data-page-title | document.title | Title stored with the thread. Shown in the dashboard. |
| data-lang | auto (browser) | Widget UI language: "en", "ko", "it", "zh" (Simplified Chinese), or "el" (Modern Greek). Omit to follow each visitor's browser language (falling back to English). Set it to pin one language for every visitor. |
| data-theme | auto (your page) | "light", "dark", or any hex color (e.g. "#FAF7F2") for a custom background. Omit and the widget matches the background of the page it sits on, falling back to the visitor's OS preference only when the page has none. |
| data-theme-source | (none) | CSS custom property to read from the host page as the widget background, e.g. "--color-bg". Useful for themes with user-selectable color schemes. |
| data-accent-color | #4f46e5 | Custom accent color for the send button and footer link. |
| data-auth-button-style | labelled | Set to "icons" to render social sign-in buttons as icon-only 44px buttons with accessible provider labels. |
| data-font | system | Typography preset: "system", "sans", "serif", "mono", or "rounded". data-font-family overrides this. |
| data-font-family | system stack | Override the widget font. Any CSS font-family value, e.g. "'Inter', sans-serif". Use "inherit" to pick up your site's font. |
| data-font-size | 14px | Override the base font size. Any CSS length, e.g. "15px" or "0.95rem". |
| data-radius | 8px | Border radius for the widget container and cards. Any CSS length, e.g. "0" for sharp corners or "16px" for softer cards. |
| data-mobile-gap | 6px | Row gap between avatar and composer on mobile (≤480px). Tighten with "0" or "4px" for narrow sidebars, or widen for more breathing room. |
| data-mobile-avatar-size | 28px | Avatar size on mobile (≤480px). Defaults to 28px so the composer card uses more horizontal space; "24px" gives the editor even more room in very narrow columns. |
| data-mobile-min-width | auto | Minimum widget width on mobile (≤480px). Set to "100%" to fill the host column edge-to-edge. |
| data-structured-data | true | Set to "false" to stop the widget from auto-injecting its schema.org DiscussionForumPosting JSON-LD — use this only if you already render the same document server-side (SSR) yourself, to avoid a duplicate block. |
Full example
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="https://example.com/blog/my-post"
data-identifier="blog-my-post"
data-page-title="My Blog Post"
data-theme="dark"
data-theme-source="--color-bg"
data-accent-color="#e8353a"
data-auth-button-style="icons"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Theming
EchoThread supports light and dark themes out of the box. By default it matches your page: the widget reads the background it is sitting on and picks the theme that belongs there, so a visitor whose device is in dark mode does not get a dark widget on your light page. If your page paints no background of its own, the visitor's operating system preference decides.
Force a theme
Set data-theme="light" or data-theme="dark" on the container to pin the theme instead. You can also pin it for a whole site — without editing the snippet — under Widget theme in that site's settings; a data-theme on the container wins over it.
Custom background color
Pass any hex color to data-theme to use it as the widget background. Text and surface colors adapt automatically based on the color's luminance.
<div id="echothread"
data-theme="#FAF7F2"
...></div>Follow a site theme variable
If your theme changes background colors with a CSS custom property, pass that property name to data-theme-source. EchoThread reads the current value as its background and updates when the host theme attribute changes.
<!-- Example: Ghost/Tuuli-style theme variables -->
<div id="echothread"
data-theme-source="--color-bg"
data-accent-color="#a31e13"
...></div>Custom accent color
Use data-accent-color to match EchoThread to your brand. This applies to the submit button, active states, and footer link. Accepts any valid CSS color value.
Need examples before choosing? Paste your URL into the widget theme builder, or browse the widget theme gallery for light, dark, localized, background-color, accent, and icon-login combinations.
/* Example: override via CSS custom properties */
#echothread {
--et-color-accent: #6366f1;
--et-radius: 12px;
--et-font: 'Inter', system-ui, sans-serif;
}Match your site's typography
By default EchoThread uses a clean system font stack so it looks consistent across browsers. Pass data-font-family to use a specific font, or data-font-family="inherit" to pick up the font your site already uses. You can also tune the base size with data-font-size and the corner radius with data-radius.
<!-- Inherit your site's font, bump the base size, soften the corners -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-font-family="inherit"
data-font-size="15px"
data-radius="12px"></div>
<!-- Or pin a specific font -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-font-family="'Inter', system-ui, sans-serif"></div>The widget defends its own typography against host-page CSS resets, so these attributes are the supported way to change fonts — host-side !important overrides on .et-widget descendants won't take effect.
CSS custom properties
| Variable | Default | Description |
|---|---|---|
| --et-color-accent | #4f46e5 | Primary accent color (send button, links, active states). |
| --et-color-accent-hover | #4338ca | Accent color used on hover. |
| --et-color-accent-text | derived | The accent used for text (links, the selected sort option, @mentions, the footer). The widget derives this from your accent and the theme background, lightening or darkening it only as far as it needs to stay readable at WCAG AA. Buttons and other filled shapes always use your exact accent. Set it yourself with !important if you would rather pick the value. |
| --et-color-bg | #ffffff | Widget background. Derived automatically from data-theme when set. |
| --et-color-surface | #f8fafc | Surface color for comment bubbles, inputs, and chips. |
| --et-color-border | #e2e8f0 | Border color for the container, inputs, and cards. |
| --et-color-text | #1e293b | Primary text color. |
| --et-color-muted | #475569 | Muted text color (timestamps, placeholders, secondary actions). |
| --et-radius | 8px | Border radius for the container and cards. |
| --et-font | system stack | Font family for all widget text. |
| --et-font-size | 14px | Base font size for the widget. |
Features
Threaded replies
Conversations nest up to 5 levels deep. Each reply includes a "Replying to" indicator so readers can follow the conversation flow, even in busy threads.
Reactions
Readers can react to comments with Like, Love, Haha, and Angry by default (Sad and Fire are also available per site). Reactions are shown inline and don't require typing a reply, keeping threads focused.
Image attachments
Commenters can upload JPEG, PNG, GIF, or WebP images directly into their comments. Images are automatically resized, optimized, and served from a CDN.
Link previews
When a commenter pastes a URL, EchoThread automatically fetches Open Graph metadata and renders a rich preview card with title, description, and thumbnail.
Text formatting & spoiler tags
Comments support four formatting primitives via toolbar buttons, keyboard shortcuts, or inline markdown:
**text**Ctrl + B_text_Ctrl + I~~text~~Ctrl + Shift + S||text||Ctrl + Shift + Xaria-label announces the current state. WCAG 2.1 AA aligned — useful for movie, TV, gaming, and book threads.GIFs
The comment box has a GIF button next to the image button. It opens a picker that shows trending GIFs from KLIPY, with a Search KLIPY box for anything else. Pick a tile and it appears above the Post button as a removable preview.
- One GIF per comment. Choosing another replaces the first. A comment can be just a GIF, with no text.
- Top-level comments only. Like image uploads, the GIF button is not shown in nested reply boxes.
- Editing: authors can remove or swap the GIF when they edit their comment. Swapping in a different GIF sends a published comment back to your moderation queue, unless you or one of your moderators made the edit.
- Content filter: every search uses KLIPY's strictest filter setting.
- Moderation: GIF comments go through the same queue and the same approve, reject, and spam actions as any other comment. The spam filter reads comment text, so a GIF with no text follows your auto-approve setting, and a guest's GIF-only comment still waits for review.
- Keyboard: Escape closes the picker and returns focus to the GIF button.
On by default, on every plan. GIFs are included on every plan, Free included. To turn them off for a site, open My Sites → [your site], scroll to Site Settings → Moderation settings, switch off GIFs in comments, and save. With the setting off, the button disappears and the widget makes no requests to KLIPY. GIFs that were already posted keep showing.
Privacy: the search and the GIF itself are loaded by the reader's browser straight from KLIPY, so KLIPY receives that reader's IP address, browser details, and search words. EchoThread stores only a link to the chosen GIF, never the file. See the privacy policy.
Sorting
Readers can sort comments by newest, oldest, most liked, or most replied. The sort preference persists for the session.
Reader polls (Starter and above)
Each page can carry one poll, shown above the comment box. Write it under Polls in your site's menu: pick the page, enter a question and 2 to 6 answers, and save. You can also add one right on the page — while you are signed in to the widget as the site's owner, an Add a poll button appears where the poll will go.
- Voting: readers vote without signing in, one vote each. "See results" shows the bars without voting.
- Editing: the question and answers can be changed until the first vote; after that they are locked so nobody's vote changes meaning. You can close a poll at any time, and the final results stay visible.
- Privacy: to stop repeat votes we store a one-way, per-poll hash of the voter's network address and browser, never the address itself, and we delete it 30 days after the poll closes.
Star ratings (Starter and above)
Turn on Show star ratings on this site in the site's settings and choose what your pages are about (Recipe, Product, Book, Course and so on). Signed-in readers can then add 1 to 5 stars to a top-level comment, and the thread shows the page's average and star breakdown. Only approved comments count.
The widget also adds schema.org AggregateRating and up to 20 Review entries to the page. If your page already has JSON-LD for an item of the type you chose, the rating is added to that item; otherwise the widget adds a small item of its own named after the page. If the item already carries a rating from another plugin, the widget leaves it alone. Whether a search engine shows stars for a page is up to the search engine — check a page with Google's Rich Results Test.
To leave a page out (an About page, say), add data-ratings="false" to its embed snippet. data-structured-data="false" turns this markup off too.
Bluesky and Mastodon
Sign-in, on every plan. Readers can sign in with their Bluesky handle or their Mastodon server, alongside the other providers. Choose which providers your site offers under Sign-in methods in the site's settings.
Replies in your thread (Starter and above). In the site's settings, connect the site's Bluesky or Mastodon account, then give a page its post: paste the link to the post you shared it in, or share the page from the dashboard in one click. Replies to that post appear in the page's thread with a Bluesky or Mastodon badge and a link back, and go through your moderation queue like any other comment. A reader who signed in with Bluesky or Mastodon can tick "Also reply on Bluesky" (or Mastodon), and once their comment is approved it is posted from their own account as a reply to your post.
Bluesky handle
Enter the handle you sign in with, such as yoursite.bsky.social. If your site's own domain is your handle, enter that; Bluesky explains how to set one up in How to verify your Bluesky account.
Mastodon server
Enter the part of your address after the second @. For @news@mastodon.social, that is mastodon.social. No account yet? See Mastodon's guide to signing up for an account.
- Which Mastodon posts work: the post you link must be Public. Replies set to Public or Quiet public appear in your thread; followers-only replies and private mentions don't. Mastodon explains the settings in Posting to your profile.
- What connecting allows: you approve the connection on Bluesky's or Mastodon's own page, through their standard sign-in (Bluesky's OAuth, Mastodon's OAuth). EchoThread never sees your password. Your account only posts when you click Share; readers' replies go out from their own accounts. Disconnect any time from the same settings card.
Languages & localization
The widget interface — buttons, labels, prompts, relative timestamps, and dates — is fully localized in English, Korean (한국어), Italian (italiano), Simplified Chinese (简体中文), and Modern Greek (Ελληνικά). By default each visitor sees the widget in their own browser language when it's supported, falling back to English otherwise, so a Greek reader and an English reader on the same page each get their own language with zero configuration.
To pin the widget to a single language for every visitor, set data-lang="el" (or "en" / "ko" / "it" / "zh") on the container. Dates and "5 minutes ago"–style timestamps are formatted with the browser's native Intl APIs, so they read naturally in each language. Comment content is never translated — only the widget's own interface.
Accessibility
The widget is built with WCAG 2.1 Level AA success criteria in mind: visible focus rings on buttons, inputs, and the compose editor; aria-labels on the compose toolbar, image-remove, and lightbox-close controls; a focus-trapped image lightbox with Escape-to-close; and broad prefers-reduced-motion coverage across animated affordances. No configuration required.
Structured data (schema.org)
On every render, the widget automatically injects a schema.org DiscussionForumPosting JSON-LD block into your page's <head>, with each comment nested as a Comment. This helps search engines and AI answer engines understand — and cite — the conversation on your page. No configuration required. Because it's injected by JavaScript, it's read by crawlers that execute JavaScript, including Googlebot's rendering pass.
Every document also carries a publisher node crediting EchoThread as the discussion platform — a permanent, machine-readable citation trail so AI answer engines and search crawlers can trace a cited conversation back to its source.
Avoiding duplicate structured data
To reach crawlers that don't run JavaScript — most AI answer bots, and the initial-HTML SEO pass — fetch the same document server-side (SSR) and render it into the page's raw HTML instead. First resolve the thread, then fetch its JSON-LD:
// Server-side — resolve the thread once (creates it on first visit,
// same as the widget itself does), then fetch and cache its JSON-LD.
const thread = await fetch(
'https://api.echothread.io/api/v1/embed/thread?api_key=' + API_KEY +
'&identifier=' + encodeURIComponent(PAGE_IDENTIFIER) +
'&page_url=' + encodeURIComponent(PAGE_URL)
).then(r => r.json())
const res = await fetch(
'https://api.echothread.io/api/v1/embed/threads/' + thread.id +
'/discussion.jsonld?api_key=' + API_KEY
)
const jsonld = await res.text() // already HTML-escaped, safe inside a <script> tag
// <script type="application/ld+json">...jsonld...</script>The endpoint returns a single DiscussionForumPosting document (or {} for a thread with no approved comments yet — emit nothing in that case), only ever includes approved comments, and is cacheable for 60 seconds.
If you SSR your own copy, set data-structured-data="false" on the widget container so it doesn't also inject a second, duplicate block — both documents share the same @id, and two conflicting copies on one page is worse for SEO than one:
<!-- Opt the widget out of its own JSON-LD since you're rendering the
fetch() result above yourself -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-identifier="blog-my-post"
data-structured-data="false"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Only set this if you're actually rendering the JSON-LD yourself. data-structured-data="false" doesn't reduce structured data on your page — with no SSR replacement, it removes all of it.
Validate the output with Google's Rich Results Test.
Authentication
How readers post is configurable per site. You can require an account or open commenting to guests — either way, the Siftfy ML spam filter screens new comments and gives moderators a clean queue.
Guest commenting
Turn on Allow guest comments in a site's moderation settings and readers can post with just a display name — no account required. Every guest comment is always scored by Siftfy before it can appear, even when auto-approve is on, so you get frictionless commenting without the spam. Guest commenting is off by default; enabling it is a one-click, per-site choice.
Commenter sign-in
Visitors can also sign in with Google, GitHub, X, Facebook, Discord, Bluesky or Mastodon. No passwords to manage, no separate accounts to create. The sign-in flow happens inside the widget without navigating away from your page. Choose which of these your site offers under Sign-in methods in the site's settings.
Sign in with EchoThread
On Starter and above you can also offer Sign in with EchoThread. Readers comment with their own EchoThread name and photo, which they edit once and which follow them to every site that offers it. If a reader has turned on a public profile, their name links to it. Readers sign up free, with a magic link or a passkey.
It is off by default and separate from "Allow all providers": turn it on under Sign-in methods in your site's settings. On Hobby the option is shown but locked.
Commenter profiles
Anyone with an EchoThread account can edit their name, photo, bio and website under Profile in the account menu. Signing in again with Google, GitHub, X, Facebook or a magic link never overwrites them. A public profile page at echothread.io/u/your-handle is optional and off until the reader turns it on. It shows only their name, photo, bio and website link, never their email or comments.
Use data-auth-button-style="icons" when a narrow theme column makes full "Continue with ..." buttons feel cramped. The buttons render as icon-only 44px targets with accessible labels, so screen readers still announce the provider names.
Site owner sign-in
Site owners use magic link authentication. Enter your email, click the link we send, and you're in. No password to remember or reset.
Moderation
EchoThread gives you full control over what appears on your site.
Moderation dashboard
The dashboard lets you approve, reject, or delete comments with a single click. Filter by status (pending, approved, rejected, spam) to work through your queue efficiently.
Auto-approve mode
Toggle auto-approve per site. When enabled, new comments go live immediately without manual review. When disabled, all comments start as "pending" until you approve them.
Spam filter
EchoThread scores every incoming comment with a machine-learning spam classifier. High-confidence spam is kept out of the public thread automatically; borderline comments land in your spam queue, tagged with the signals that flagged them — link density, ALL-CAPS, repeated characters, promo keywords, and drive-by links — so you can review at a glance. Behind the scenes, each comment's spam probability is scored by the Siftfy classification API: very high scores auto-block, borderline scores go to your review queue, and the rest pass through.
Restricted words
Keep a blocklist of your own on each site. Every new comment is checked against it — both the comment body and the author's display name — before the spam classifier runs, so your own rule decides first and a comment your list catches never goes to the classifier at all.
Pick what a match does for the whole list, not word by word: hold sends the comment to your moderation queue (the default), or reject files it as rejected — the same status as a comment you reject by hand, so it stays in the queue under Rejected and can be approved later. A match on the author's display name always holds and never rejects: a name is not a message.
Matching is case-insensitive. * is a wildcard that matches a run of non-space characters, and a space in an entry matches any run of whitespace, so a phrase pasted with odd spacing still matches. Entries that begin or end with an ASCII letter, digit, or underscore are matched on word boundaries — ass doesn't fire on class — while entries in scripts those boundaries don't apply to, such as Chinese, Arabic, or Hebrew, are matched as substrings rather than left unmatchable.
A list holds up to 2,000 entries of up to 128 characters each, enforced on the server. In the queue, a comment your list caught is labelled as your own rule rather than as spam and shows the text that matched — both details are visible to you and your moderators only, like the spam score, and never to the commenter. The list travels with your data: it's included in the site's JSON export under a moderation object. Only the site owner can edit the list; moderators can't change it.
Banning and trusting commenters
Each site keeps its own standing on the people who comment there. Ban stops someone posting to that site. Trust auto-approves their comments on it. Both are per site and per person — never platform-wide, so your decision never follows a reader to anyone else's site.
A trusted commenter skips pre-moderation and a restricted-word hold, but never a restricted-word reject — that's a rule about content, not about a person.
A ban can optionally reject that person's still-visible comments from the last 30 days. It's opt-in and never the default, and those comments are rejected rather than deleted, so the decision is reversible. On a very busy site the sweep examines a bounded window of recent comments and tells you when it stopped early, so you can check the Rejected tab for any it didn't reach.
Owners and moderators can both ban and trust — it's queue work. People who hold a seat on the site can't be banned or trusted.
This isn't the same thing as a reader blocking someone. A reader's block hides that person from that one reader and tells nobody; a ban removes them from your site.
Auto-closing old threads
Set auto-close to 30, 60, 90, 180, or 365 days and a thread stops taking new comments that many days after it was created. Leave it off — the default — and threads stay open indefinitely.
Existing comments stay visible and readable; closing a thread only stops new ones. The window is worked out when the page is requested rather than written onto your threads, so lengthening the setting or turning it off reopens the threads it had closed. A thread you re-open by hand is exempt from the schedule from then on.
The widget renders a closed thread read-only with a plain explanation, and signed-out readers see that notice too. Thread responses carry a closed_reason of "closed", "locked", or "auto_close", so a scheduled close is distinguishable from one somebody made by hand.
Notifications
Get dashboard notifications when new comments arrive or spam is detected. The notification bell polls for unread counts so you never miss activity.
Site settings
Each site you register has its own configuration. Navigate to My Sites → [your site] in the dashboard to manage these.
| Setting | Description |
|---|---|
| Site name | Display name for your site (shown in the dashboard only). |
| Allowed domains | Every domain your pages are published on, separated by commas. Subdomains are included. The widget shows an error on any other domain. |
| Description | Optional description for your own reference. |
| Auto-approve | When on, new comments are immediately visible. When off, they require manual approval. |
| Spam filter | When on, likely spam is automatically flagged and sent to the spam queue. |
| Allow voting | Show or hide emoji reactions (Like, Love, Haha, Angry) on comments. |
| GIFs in comments | Show the GIF button in the comment box. On by default. Turning it off hides the button; GIFs already posted keep showing. |
| Restricted words | Your own blocklist for this site, checked against the comment body and the author's display name before the spam filter runs. Up to 2,000 entries of 128 characters each; * is a wildcard. Owner-only. |
| Restricted word action | What a restricted-word match does, for the whole list: hold for review (the default) or reject. A display-name match always holds. |
| Auto-close threads | Stop new comments 30, 60, 90, 180, or 365 days after a thread is created. Off by default. Existing comments stay visible, and clearing the setting reopens the threads. |
API key
Each site has a unique API key that authenticates the embed widget. You can find it on your site's settings page in the dashboard.
Your API key is public, and that's expected. It sits in your page's HTML, so anyone can see it. EchoThread only runs the widget on your site's Allowed domains (plus local development addresses), so other websites can't embed your comments. Treat this as a safeguard, not a password: scripts outside a browser can still call the API, so keep moderation and spam filtering on, and regenerate the key if you think it's being misused.
Regenerating your key
If your API key is compromised, click Regenerate on your site settings page. The old key stops working immediately. Update the embed snippet on your site with the new key.
Webhooks
On the Starter plan and above, subscribe an HTTPS endpoint to comment and thread events — new comments, approvals, rejections, deletions, new threads — and get a signed request the moment they happen instead of polling the API. The same screen, Alerts and webhooks on each site, also sends new comments to a Slack or Discord channel: paste the channel's incoming-webhook URL, choose whether to hear about comments awaiting review, published comments, or both, and press Send test. Comments that ask a question are marked, and an alert never includes a commenter's email or IP address. See the full webhooks documentation for event payloads, the X-EchoThread-Signature verification scheme, and the retry schedule.
Public API
Read your sites, threads and comments from your own server on every plan — the free Hobby plan can create up to 2 read-only tokens — and, on the Starter plan and above, moderate comments — approve, reject, mark spam, reply, delete — authenticated with a personal API token from Account → API Tokens. Creating comments through the API isn't supported — comments are only ever posted by a real visitor through the embed widget. See the full public API documentation for every route, cursor pagination, and error shapes.
MCP server
Manage your comments from Claude, ChatGPT desktop, Cursor or any other assistant that speaks the Model Context Protocol. The EchoThread MCP server, @echothread/mcp, runs on your own computer and calls the public API with a personal API token, so it can do exactly what that token's scopes and your plan allow: read on every plan, and moderate, reply and delete on Starter and above. It is open source (MIT) and listed in the official MCP Registry as io.echothread/mcp.
- Create a token on Account → API Tokens. Tick only the scopes the assistant needs.
- Add the server to your assistant's MCP configuration, with the token in
ECHOTHREAD_API_TOKEN:
{
"mcpServers": {
"echothread": {
"command": "npx",
"args": ["-y", "@echothread/mcp"],
"env": { "ECHOTHREAD_API_TOKEN": "et_..." }
}
}
}It offers 11 tools: list your sites, the moderation queue, new comments since a time, get a thread with its comments, get a comment, per-site comment stats, approve, reject, mark spam, reply, and delete. Delete is permanent, so give the token the delete scope only if you want the assistant to have it. A tool the token has no scope for returns a clear message naming the missing scope, and nothing changes. The server cannot post new comments: comments come only from real visitors through the widget.
n8n
On the Starter plan and above, the EchoThread community node for n8n starts a workflow for each new comment and lets the workflow act on it. Install @echothread/n8n-nodes-echothread from Settings → Community Nodes in n8n, then add an EchoThread credential with a personal API token.
- EchoThread Trigger — New Comment: polls every site your account owns and runs once per new comment, optionally only for the statuses you pick (pending, approved, spam or rejected). On first activation it starts from that moment, so old comments don't flood the workflow.
- EchoThread: approve, reject, mark spam, reply to, or get a comment by id.
A token without the scope an action needs fails that item with the API's scope_required message; a Hobby token gets "The n8n node needs Starter". The node is open source (MIT); see its source on GitHub.
Single sign-on (SSO)
On the Pro plan and above, sign your readers into EchoThread automatically using the identity they already have on your site: your backend HMAC-signs a small {id, email, name, avatar} payload with your site's SSO secret and passes it to the widget, so a visitor already signed in on your side lands signed in here too, with no second login step. See the full SSO documentation for the payload shape, worked Node and PHP signing examples, the 5-minute timestamp window, and secret rotation.
Platform guides
EchoThread works on any website that supports custom HTML — from static site generators to hosted no-code builders like Shopify, Wix, Framer, Squarespace, and Hostinger. Static site generators are especially popular with EchoThread since they have no built-in backend for comments, and no-code builders pair well with it because their native comment tools are limited. For a deeper comparison, see our guide to the best commenting system for static sites. Below are detailed setup guides for the most popular platforms.
Static HTML
Static HTML is the simplest integration. No build tools, frameworks, or dependencies — just copy and paste.
Step 1 — Add the embed snippet
Paste the following code in your HTML file where you want the comment section to appear. A good location is after your article content, before the closing </body> tag:
<!-- Add this where you want comments to appear -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="https://example.com/blog/my-post.html"
data-identifier="my-post"
data-page-title="My Blog Post"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Step 2 — Set your attributes
Replace the placeholder values with your own:
data-api-key— your site's API key from the EchoThread dashboard (My Sites → Settings).data-page-url— the full canonical URL of the page. This should be the same URL visitors see in their browser.data-identifier— a unique, stable string for this page (e.g. a slug like"my-post"). This ensures comments stay linked to the page even if you change the URL later.data-page-title— the page title shown in your moderation dashboard and notifications.
Step 3 — Customize (optional)
Add optional attributes to control the look and feel:
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="https://example.com/blog/my-post.html"
data-identifier="my-post"
data-page-title="My Blog Post"
data-theme="dark"
data-accent-color="#6366f1"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Full page example
Here's a minimal complete HTML page with EchoThread:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Blog Post</title>
</head>
<body>
<article>
<h1>My Blog Post</h1>
<p>Your post content here...</p>
</article>
<!-- EchoThread comments -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="https://example.com/blog/my-post.html"
data-identifier="my-post"
data-page-title="My Blog Post"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>
</body>
</html>Multi-page sites: Each page needs its own unique data-identifier. If all your pages share the same identifier, all comments will appear on every page. Use the page slug, filename, or any unique string.
WordPress
WordPress has a built-in comment system, but many site owners prefer EchoThread for its modern UI, threaded replies, reactions, and ML spam filtering. The easiest way is our official plugin — no code required. If you want more control, you can also add EchoThread by hand in three ways.
Recommended — Official WordPress plugin (no code)
Install Comments – EchoThread from the WordPress.org plugin directory — or search EchoThread under Plugins → Add New in your dashboard. Activate it, open Settings → EchoThread Comments, paste your site shortname and API key, and pick where comments appear: replace your theme's comments, append below the content, or drop them anywhere with the [echothread] shortcode. No theme edits, no database tables.
Turn on Archive comment counts while you're there. WordPress stops updating its own comment counts once EchoThread hosts the discussion, so category and home-page teasers keep showing whatever the number was on the day you switched — or zero. With the setting on, those counts come from EchoThread instead. See Comment counts.
Prefer to wire it up by hand? Three more ways:
Option A — Custom HTML block (per post)
This is the quickest way to try EchoThread on a single page. Open the post or page in the WordPress block editor, add a Custom HTML block where you want comments, and paste the snippet:
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="<?php the_permalink(); ?>"
data-identifier="wp-<?php the_ID(); ?>"
data-page-title="<?php the_title(); ?>"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Note: PHP tags (<?php ... ?>) only work inside theme template files, not in the block editor. If you're using the block editor, hardcode the page URL and a unique identifier instead, or use Option B for automatic values.
Option B — Theme template (site-wide, recommended)
To add EchoThread to every post automatically, edit your theme's single post template. This is the recommended approach for most WordPress sites.
Open single.php (or singular.php) in your active theme and add the snippet after the post content:
<!-- In your theme's single.php, after the_content() -->
<?php if ( comments_open() || get_comments_number() ) : ?>
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="<?php echo esc_url( get_permalink() ); ?>"
data-identifier="wp-<?php the_ID(); ?>"
data-page-title="<?php echo esc_attr( get_the_title() ); ?>"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>
<?php endif; ?>Option C — Replace the default comment system
To completely replace WordPress's default comments with EchoThread, create a custom comment template. Add this to your theme's functions.php:
// functions.php — Load EchoThread instead of default comments
function echothread_comments_template( $template ) {
return get_stylesheet_directory() . '/echothread-comments.php';
}
add_filter( 'comments_template', 'echothread_comments_template' );Then create echothread-comments.php in your theme folder:
<!-- echothread-comments.php -->
<div id="echothread"
data-api-key="YOUR_API_KEY"
data-page-url="<?php echo esc_url( get_permalink() ); ?>"
data-identifier="wp-<?php the_ID(); ?>"
data-page-title="<?php echo esc_attr( get_the_title() ); ?>"></div>
<script src="https://cdn.echothread.io/widget.js" async></script>Child themes: If you're using a child theme, place echothread-comments.php and functions.php edits in the child theme folder so your changes survive theme updates. This approach works with all major themes including Astra, GeneratePress, Kadence, and block themes (FSE).
Disable WordPress's default comments (optional)
If you're fully switching to EchoThread, you can disable the built-in comment system to avoid confusion:
// functions.php — Disable default WordPress comments entirely
add_action( 'admin_init', function() {
// Remove comments from admin menu
remove_menu_page( 'edit-comments.php' );
// Disable comments on all post types
foreach ( get_post_types() as $post_type ) {
remove_post_type_support( $post_type, 'comments' );
remove_post_type_support( $post_type, 'trackbacks' );
}
});For React frameworks and static site generators, follow the dedicated guide for your platform. Each guide walks through the reusable component or template include, identifier choices, conditional rendering, and any framework-specific gotchas.
Next.js / React
Reusable React component with App Router and Pages Router setup.
Read the full Next.js / React guideHugo
Partial template with per-post identifiers and multilingual support.
Read the full Hugo guideJekyll
Liquid include for Jekyll blogs and GitHub Pages.
Read the full Jekyll guideAstro
Astro component with content collections support.
Read the full Astro guideEleventy (11ty)
Nunjucks, Liquid, or Handlebars include with per-collection toggles.
Read the full Eleventy (11ty) guideGatsby
React component that loads after Gatsby hydrates.
Read the full Gatsby guide
Hosted website builders don't expose theme files the way a static site generator does, but each one has a place for custom HTML — so the same embed snippet works with no build step. The guides below cover the platform-specific placement; for the full walkthrough, follow the linked companion guide for each platform.
Framer
Framer's Embed element accepts raw HTML, so EchoThread drops in without code. To add comments to every blog post at once, edit the CMS Collection page rather than an individual page.
- Open the CMS Collection page (or a static page) and drag an Embed element below your content.
- Set the embed type to HTML and paste the Quick start snippet.
- For blog posts, use the code editor's + menu to bind the post slug to
data-identifierso each post gets its own thread. - Set the element width to Fill and height to Auto so the thread can grow, then publish.
Wix
Use Wix's Custom Code feature, not the Embed HTML element. Wix's own documentation states that its HTML element is an iframe, so code inside it runs on a Wix address rather than your domain — which means commenter sign-in cannot complete. Custom Code injects into the page itself.
- Publish the site and connect your domain. Wix requires both before Custom Code runs, and EchoThread needs the domain registered in your dashboard before sign-in will complete.
- In your Wix dashboard open Settings → Custom Code and add a snippet placed in Body – end.
- Paste a short script that creates the
<div id="echothread">container and then loads the widget — the full snippet is in the Wix guide below. - Scope it to your blog post pages rather than all pages, and set it to load on each new page so it follows client-side navigation.
Shopify
Shopify blog posts are rendered by your theme's article template. Add EchoThread by editing that template in the theme code editor — no app required.
- From the Shopify admin, go to Online Store → Themes, then … → Edit code. Duplicate the theme first so you can test safely.
- Open
sections/main-article.liquid(ortemplates/article.liquidon older themes). - Paste the Quick start snippet just after
{{ article.content }}, usingdata-identifier="{{ article.id }}"so each post keeps its own thread. - Replace the native
{% if blog.comments_enabled? %}block rather than adding beside it, then save and preview. - Disable Shopify's own comments under Content → Blog posts → Manage blogs, or it keeps accepting them behind the scenes.
Squarespace
Squarespace supports EchoThread through a Code Block — a no-code installation that takes a few minutes.
- Edit a blog post (or the blog post template) and add a Code Block below the content.
- Paste the Quick start snippet and confirm the block is set to render HTML, not display it as text.
- On the Core, Plus and Advanced plans (code injection is not on the entry-level plan) you can instead paste the whole snippet once into the blog page's Advanced → Post Blog Item Code Injection field, which applies it to every post in that blog — including ones you write later.
- Turn off Squarespace's own comments in the blog's Comments Settings, or you will have two comment boxes on the same post.
Hostinger Website Builder
Hostinger keeps the <script> from a pasted snippet but not the <div id="echothread"> container, so the widget has nothing to mount into and the post stays blank. This is not an iframe problem — Hostinger renders its Embed element inline in your page, on your own domain. Build the container in JavaScript instead and it works.
- Register your domain in your EchoThread dashboard first, or comments will load and sign-in will silently fail.
- In the builder, open the … menu, choose Integrations, and paste the snippet into Custom code — not into an Embed code element.
- Use a script that creates the
<div id="echothread">itself. Pasting the div as HTML is what fails. - Gate it on
.block-blog-headerso only blog posts get a thread, and setdata-identifierfrom the page path so each post keeps its own discussion. - Publish the site — custom code does not run in the builder preview.
Ghost
Ghost has had its own comments since 5.0, but they are members-only — a reader has to sign up to your publication before posting. Both default themes, Source and Casper, render them from a {{#if comments}} block in post.hbs, so replacing that block swaps members-only comments for open ones.
- Register your Ghost site's domain in your EchoThread dashboard so sign-in can complete.
- Download the active theme from Settings → Design & branding → Theme. You do not need a local Ghost install.
- In
post.hbs, replace the{{#if comments}}block with the EchoThread container and script, usingdata-identifier="{{id}}"anddata-page-url="{{url absolute="true"}}". - Re-upload the theme. Use
{{id}}rather than{{slug}}— editing a post title rewrites the slug, which would strand the discussion.
Running a traditional CMS? Each of these has a dedicated guide covering the exact file to edit, which identifier survives a URL change, and how to switch off the platform's own comments so you don't end up with two.
Ghost
Replace the members-only comments block in post.hbs on Source or Casper.
Read the full Ghost guideShopify
Swap the native comment form in sections/main-article.liquid — no app needed.
Read the full Shopify guideSquarespace
Post Blog Item Code Injection, applied once to every post in a blog.
Read the full Squarespace guideWix
Custom Code, not the HTML embed — the embed is an iframe and breaks sign-in.
Read the full Wix guideHostinger Website Builder
Integrations custom code — the container must be built in JS, not pasted.
Read the full Hostinger Website Builder guideBlogger
Theme XML with expr: attributes so threads key to the post ID.
Read the full Blogger guideDrupal
A Twig template override — text formats strip a pasted script tag.
Read the full Drupal guideJoomla
A Cassiopeia override that survives core updates. No extension.
Read the full Joomla guide
Import and export reference
Load the detailed Disqus, WordPress, JSON, and CSV field reference when you are ready to move comment data.
Migrating from Disqus? Start with the safe Disqus export guide.
Troubleshooting
Widget doesn't appear
- Check that the
data-api-keymatches the key in your dashboard. - Make sure the widget script URL is correct:
https://cdn.echothread.io/widget.js - Check that the page's domain is in Allowed domains in your site settings. The widget shows an error on any other domain.
- Open your browser's developer console and look for error messages.
Comments not showing up
- If auto-approve is off, new comments stay in "pending" until you approve them in the dashboard.
- Check the moderation queue for pending or spam-flagged comments.
"This domain isn't allowed" errors
- Add every domain your pages are published on to Allowed domains in your site settings. Subdomains are included. See ET_DOMAIN_NOT_ALLOWED.
example.comandhttps://example.comboth work.- You don't need to change anything to test locally:
localhost,*.localhost,*.test,*.localand private network addresses always work.
Wrong thread / duplicate threads
- Use
data-identifierto tie a thread to a stable ID (e.g., a post slug). Without it, threads default to the page URL, which can cause duplicates if URLs change. - Use
data-page-urlto set the canonical URL if your pages are accessible via multiple URLs.
FAQ
Is EchoThread free?
Yes. The Hobby tier is free for your first site, with no credit card required. It is a permanent free plan; paid plans start at $5/month if you outgrow it.
Does EchoThread use cookies?
No. EchoThread does not set any cookies. When a commenter signs in, their session tokens are kept in your browser's local storage — not in cookies, and never shared with third parties.
Does EchoThread track my visitors?
No. There are no analytics, no ads, and no third-party tracking scripts. We collect only the data needed to display and moderate comments.
How big is the widget?
Under 65 KB gzipped. Zero dependencies. It loads asynchronously and won't block your page rendering.
Can I export my data?
Yes. You can export all comments from any site as JSON or CSV from your site settings page. The JSON export uses the same format as the import, so you can easily migrate between sites. See the Exporting comments section above.
Will the free tier ever go away?
No. The Hobby tier is a permanent free plan. Paid plans exist for high-volume sites, but a generous free tier will always be available.
Is there a WordPress plugin?
Yes. Install "Comments – EchoThread" from the WordPress.org plugin directory (or search EchoThread under Plugins → Add New), then paste your site shortname and API key — no code or theme edits. You can replace your theme's comments, append them below the content, or place them with the [echothread] shortcode. Prefer to do it by hand? The WordPress section above covers the embed snippet and theme-template methods too.
Can guests comment without signing in?
Yes, if the site owner enables it. Turn on "Allow guest comments" in a site's moderation settings and readers can post with just a display name — no account required. Every guest comment is screened by the Siftfy ML spam filter first. Owners can also require Google, GitHub, X, Facebook, Discord, Bluesky, or Mastodon sign-in instead. Guest commenting is off by default.
Does the widget support other languages?
Yes. The widget interface is localized in English, Korean, Italian, Simplified Chinese, and Modern Greek. By default each visitor sees it in their own browser language, falling back to English — no setup required. You can pin a single language for all visitors with data-lang="el" (or "en" / "ko" / "it" / "zh") on the container. Timestamps and dates are formatted natively per language. Comment content itself is never translated, only the widget's own interface.
How do I add schema markup to my comments automatically?
It's already on — no setup needed. The widget injects a schema.org DiscussionForumPosting/Comment JSON-LD block into your page on every render, which covers Googlebot and other JavaScript-rendering crawlers, and every document credits EchoThread as the publisher so AI answer engines can trace a cited conversation back to its source. To also reach crawlers that don't run JavaScript (most AI answer bots), fetch the same document server-side (SSR) from the discussion.jsonld endpoint and render it into your page's raw HTML — set data-structured-data="false" on the widget container so it stops injecting its own copy once you've done that, avoiding a duplicate block. See the Structured data section above for the endpoint and a code example.
Support
Need help? Sign in to your dashboard and use the Feedback form to reach us directly.
Comment counts on listing pages
The widget renders one discussion, on one page. To show how many comments a post has from somewhere the widget isn't — a category archive, your home page, a related-posts rail — use the count script instead. It's under 4 KB gzipped and resolves every count on the page in a single request.
Mark each spot with
data-et-count, set to the same identifier the widget uses for that page, then load the script once:Each element's text is replaced only once its number arrives, so whatever you put inside is what a reader sees if the request fails or the script is blocked. The resolved number is also written to
data-et-count-value, so you can style on it — hiding a "0 comments" badge, for example.If you didn't set
data-identifieron the widget, your threads are keyed on the page URL: leavedata-et-countempty on a link and the script uses that link'shref— a trailing#echothreaddoesn't affect the match.Counts on links take the reader to the discussion: end the link at
#echothread, the widget's anchor on the post page, and the click lands with the thread in view rather than at the top of the post. A link your theme wrote for its own comments area (WordPress's#comments, say) is repointed to the widget once the count arrives; a link that already aims somewhere specific is never redirected.Wording and language
Counts read in the same language as the widget. Set
data-langto pin one (en,ko,it,zhorel), or leave it off and each visitor sees the count in their browser's language — the same choice the widget's container makes. Any single form can still be overridden with your own wording, and an override always wins:Counts also fit themselves to your page. If the colour a count would inherit is unreadable against the background behind it — black text in a dark theme's meta row, the usual case — the script repaints it with a readable one. A readable colour, and any colour you set inline on the element, is never touched.
Fetching counts yourself
The script is a thin wrapper around one public endpoint, so a static-site generator or a server-rendered template can bake the numbers straight into the HTML:
Every identifier you ask about comes back, including ones with no thread yet — those are
0. The response is cacheable for 60 seconds. Counts include replies, and match the number shown in the widget's own header.On WordPress, the EchoThread plugin does this for you: turn on Archive comment counts and your theme's existing counts are filled from EchoThread instead of WordPress's own table, which stops updating the moment EchoThread takes over.