How to add comments to a Next.js site
Add a comment system to your Next.js app with a reusable React component. Works with App Router, Pages Router, SSG, SSR, and ISR.
On this page
Why Next.js + EchoThread
Next.js is the most popular React framework, but adding comments to a statically generated or server-rendered site isn't straightforward. Most comment widgets assume a plain HTML page and break with React's hydration model.
EchoThread sidesteps this entirely. The widget is a plain JavaScript file that self-initializes by finding its container div. In a Next.js app, you just need to load the script on the client side with useEffect — one component, no npm packages, no API routes.
Prerequisites
- A Next.js project (v13+ for App Router, or any version for Pages Router)
- An EchoThread account — create one free
- Your site's API key from the EchoThread dashboard
Step 1 — Create the component
Create components/EchoThread.jsx (or .tsx):
'use client' // required for App Router
import { useEffect, useRef } from 'react'
export default function EchoThread({ pageId, pageTitle, pageUrl }) {
const loaded = useRef(false)
useEffect(() => {
if (loaded.current) return
loaded.current = true
const script = document.createElement('script')
script.src = 'https://cdn.echothread.io/widget.js'
script.async = true
document.body.appendChild(script)
return () => script.remove()
}, [])
return (
<div
id="echothread"
data-api-key="YOUR_API_KEY"
data-identifier={pageId}
data-page-title={pageTitle}
data-page-url={pageUrl}
/>
)
}
'use client'? The App Router renders components on the server by default. Since EchoThread uses useEffect and DOM APIs, it needs the 'use client' directive. This only affects this component — the rest of your page can remain server-rendered.
Step 2 — Use in a page
Import the component wherever you want comments:
import EchoThread from '@/components/EchoThread'
<EchoThread
pageId="my-post-slug"
pageTitle="My Blog Post"
pageUrl="https://example.com/blog/my-post"
/>
App Router
For dynamic blog routes with the App Router:
import EchoThread from '@/components/EchoThread'
export default function BlogPost({ params }) {
// fetch your post data...
return (
<article>
<h1>{post.title}</h1>
{/* post content */}
<EchoThread
pageId={params.slug}
pageTitle={post.title}
pageUrl={`https://example.com/blog/${params.slug}`}
/>
</article>
)
}
generateStaticParams and ISR. The component renders a container div at build time, then the widget script initializes on the client. No SSR-specific handling needed.
Pages Router
The same component works with the Pages Router. The only difference: remove the 'use client' directive, since all Pages Router components are client-rendered by default.
import EchoThread from '@/components/EchoThread'
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
{/* post content */}
<EchoThread
pageId={post.slug}
pageTitle={post.title}
pageUrl={`https://example.com/blog/${post.slug}`}
/>
</article>
)
}
export async function getStaticProps({ params }) {
// fetch post data by params.slug
return { props: { post } }
}
export async function getStaticPaths() {
// return all post slugs
return { paths: [...], fallback: false }
}
TypeScript
Here's a typed version of the component:
'use client'
import { useEffect, useRef } from 'react'
interface EchoThreadProps {
pageId: string
pageTitle: string
pageUrl: string
theme?: 'light' | 'dark' | string
accentColor?: string
}
export default function EchoThread({
pageId, pageTitle, pageUrl, theme, accentColor
}: EchoThreadProps) {
const loaded = useRef(false)
useEffect(() => {
if (loaded.current) return
loaded.current = true
const s = document.createElement('script')
s.src = 'https://cdn.echothread.io/widget.js'
s.async = true
document.body.appendChild(s)
return () => { s.remove() }
}, [])
return (
<div
id="echothread"
data-api-key="YOUR_API_KEY"
data-identifier={pageId}
data-page-title={pageTitle}
data-page-url={pageUrl}
data-theme={theme}
data-accent-color={accentColor}
/>
)
}
Theming
Pass theme props to the component:
<EchoThread
pageId="my-post"
pageTitle="My Post"
pageUrl="https://example.com/blog/my-post"
theme="dark"
accentColor="#2563eb"
/>
Options: "light", "dark", or any hex color for a custom background.
Troubleshooting
Comments don't appear
- Check your API key in the dashboard.
- Make sure the domain matches (including
localhost:3000for dev). - Open the browser console for errors.
Hydration mismatch warnings
The 'use client' directive ensures the component renders client-side. If you still see hydration warnings, verify the div isn't being rendered differently on server vs. client.
Widget re-initializes on navigation
The useRef guard ensures the script only loads once. If you're using client-side routing between blog posts, the widget container will update its data attributes and the widget will re-initialize for the new page.
Ready to add comments to your Next.js site?
Free during beta. Set up in under 5 min.
Create free account