Skip to content

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.

5 min setup No dependencies
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

Step 1 — Create the component

Create components/EchoThread.jsx (or .tsx):

components/EchoThread.jsx '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} /> ) }
Why '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:

app/blog/[slug]/page.jsx 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> ) }
Static generation: EchoThread works with 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.

pages/blog/[slug].jsx 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:

components/EchoThread.tsx '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:3000 for 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