<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Yoni Ryabinski</title>
    <description>The latest articles on Forem by Yoni Ryabinski (@yoni_ryabinski_da05ba8c9c).</description>
    <link>https://forem.com/yoni_ryabinski_da05ba8c9c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868711%2F0b764085-4f53-41da-ba88-2dd032a15ff5.png</url>
      <title>Forem: Yoni Ryabinski</title>
      <link>https://forem.com/yoni_ryabinski_da05ba8c9c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/yoni_ryabinski_da05ba8c9c"/>
    <language>en</language>
    <item>
      <title>How to Add Comments to an Eleventy Site (with a Custom Shortcode)</title>
      <dc:creator>Yoni Ryabinski</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:23:26 +0000</pubDate>
      <link>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-eleventy-site-with-a-custom-shortcode-51c7</link>
      <guid>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-eleventy-site-with-a-custom-shortcode-51c7</guid>
      <description>&lt;p&gt;Eleventy gets out of your way, which is great until you want a feature like comments and have to wire it up yourself. The cleanest answer: a hosted widget plus a custom shortcode so you can drop comments into any template with one line.&lt;/p&gt;

&lt;p&gt;Here's how to add &lt;a href="https://echothread.io/" rel="noopener noreferrer"&gt;EchoThread&lt;/a&gt; (privacy-first, no ads, under 15 KB gzipped, free during beta) to an Eleventy site.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get an API key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://echothread.io/register" rel="noopener noreferrer"&gt;echothread.io&lt;/a&gt;, add your domain, copy the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Set up env var loading
&lt;/h2&gt;

&lt;p&gt;Install dotenv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;ECHOTHREAD_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Add a shortcode in &lt;code&gt;.eleventy.js&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addShortcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;echothread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ECHOTHREAD_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;!-- EchoThread: missing key --&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;section class="echothread-wrapper"&amp;gt;
  &amp;lt;div id="echothread" data-api-key="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;script src="https://cdn.echothread.io/widget.js" defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/section&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Use it in your post layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
layout: layouts/base.njk
---
&amp;lt;article&amp;gt;
  &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;
  {{ content | safe }}
  {% echothread %}
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Make it opt-in (optional)
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;comments: true&lt;/code&gt; to a post's front matter, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% if comments %}
  {% echothread %}
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Theme it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.echothread-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fafafa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2563eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;The widget identifies threads by page URL automatically. Every post gets its own conversation, no extra config. Eleventy stays essentially zero-JS aside from the widget itself.&lt;/p&gt;

&lt;p&gt;Full guide with troubleshooting: &lt;a href="https://echothread.io/docs/guides/eleventy" rel="noopener noreferrer"&gt;echothread.io/docs/guides/eleventy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>eleventy</category>
      <category>11ty</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add Comments to a Hugo Site (Without a Backend)</title>
      <dc:creator>Yoni Ryabinski</dc:creator>
      <pubDate>Fri, 10 Apr 2026 22:20:27 +0000</pubDate>
      <link>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-hugo-site-without-a-backend-2pn</link>
      <guid>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-hugo-site-without-a-backend-2pn</guid>
      <description>&lt;p&gt;Hugo is wonderful for performance and terrible for anything stateful — including comments. If you want readers to talk back, you need a third-party widget that runs in the browser.&lt;/p&gt;

&lt;p&gt;Here's the shortest path to threaded comments on a Hugo site, using &lt;a href="https://echothread.io/" rel="noopener noreferrer"&gt;EchoThread&lt;/a&gt; (privacy-first, no ads, no tracking, under 15 KB gzipped, free during beta).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get an API key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://echothread.io/register" rel="noopener noreferrer"&gt;echothread.io&lt;/a&gt;, add your domain, copy the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the embed to &lt;code&gt;layouts/_default/single.html&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ define "main" }}
&amp;lt;article&amp;gt;
  &amp;lt;h1&amp;gt;{{ .Title }}&amp;lt;/h1&amp;gt;
  {{ .Content }}
&amp;lt;/article&amp;gt;

&amp;lt;section class="comments"&amp;gt;
  &amp;lt;div id="echothread" data-api-key="{{ .Site.Params.echothreadApiKey }}"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;script src="https://cdn.echothread.io/widget.js" defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/section&amp;gt;
{{ end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Add the key to &lt;code&gt;hugo.toml&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[params]&lt;/span&gt;
  &lt;span class="py"&gt;echothreadApiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use an env var: &lt;code&gt;HUGO_PARAMS_ECHOTHREADAPIKEY=... hugo&lt;/code&gt;. Hugo maps &lt;code&gt;HUGO_PARAMS_*&lt;/code&gt; automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. (Optional) Make a shortcode
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;layouts/shortcodes/echothread.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="echothread-wrapper"&amp;gt;
  &amp;lt;div id="echothread" data-api-key="{{ .Site.Params.echothreadApiKey }}"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;script src="https://cdn.echothread.io/widget.js" defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can drop &lt;code&gt;{{&amp;lt; echothread &amp;gt;}}&lt;/code&gt; into any Markdown file.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Theme it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.comments&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f0f10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e8e8ea&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--echothread-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#7c5cff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;min-height&lt;/code&gt; matters — it prevents layout shift when the widget hydrates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;The widget identifies threads by page URL automatically, so every post gets its own conversation with no extra config. Sign in with Google to leave a comment, manage everything from the EchoThread dashboard.&lt;/p&gt;

&lt;p&gt;Full guide with troubleshooting and CSP notes: &lt;a href="https://echothread.io/docs/guides/hugo" rel="noopener noreferrer"&gt;echothread.io/docs/guides/hugo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>staticsite</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add Comments to an Astro Site (Lazy-Loaded, Zero JS by Default)</title>
      <dc:creator>Yoni Ryabinski</dc:creator>
      <pubDate>Thu, 09 Apr 2026 20:38:55 +0000</pubDate>
      <link>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-astro-site-lazy-loaded-zero-js-by-default-hba</link>
      <guid>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-astro-site-lazy-loaded-zero-js-by-default-hba</guid>
      <description>&lt;p&gt;Astro's "ship zero JavaScript by default" model is a perfect fit for comment widgets — they're the one thing on the page that actually needs to be interactive, and they're the perfect candidate for lazy loading.&lt;/p&gt;

&lt;p&gt;Here's how to add &lt;a href="https://echothread.io/" rel="noopener noreferrer"&gt;EchoThread&lt;/a&gt; (privacy-first, no ads, under 15 KB gzipped, free during beta) to an Astro site as a reusable component.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get an API key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://echothread.io/register" rel="noopener noreferrer"&gt;echothread.io&lt;/a&gt;, add your domain, copy the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the key to &lt;code&gt;.env&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;PUBLIC_ECHOTHREAD_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;PUBLIC_&lt;/code&gt; prefix is required to expose it client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create &lt;code&gt;src/components/EchoThread.astro&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
const apiKey = import.meta.env.PUBLIC_ECHOTHREAD_API_KEY;
const { theme = "auto" } = Astro.props;
---

&amp;lt;section class="echothread-wrapper"&amp;gt;
  &amp;lt;div id="echothread" data-api-key={apiKey} data-theme={theme}&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;script src="https://cdn.echothread.io/widget.js" defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;style&amp;gt;
  .echothread-wrapper {
    margin-top: 4rem;
    padding-top: 2rem;
    border-top: 1px solid var(--border-color, #e5e5e5);
    min-height: 400px;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Use it in your post layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import BaseLayout from "./BaseLayout.astro";
import EchoThread from "../components/EchoThread.astro";
---
&amp;lt;BaseLayout title={frontmatter.title}&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;{frontmatter.title}&amp;lt;/h1&amp;gt;
    &amp;lt;slot /&amp;gt;
  &amp;lt;/article&amp;gt;
  &amp;lt;EchoThread /&amp;gt;
&amp;lt;/BaseLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. (Bonus) Lazy-load with IntersectionObserver
&lt;/h2&gt;

&lt;p&gt;If you want true zero-JS until the reader actually scrolls to the comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;section id="echothread-section"&amp;gt;
  &amp;lt;div id="echothread" data-api-key={apiKey}&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;script&amp;gt;
  const section = document.getElementById("echothread-section");
  const observer = new IntersectionObserver((entries) =&amp;gt; {
    if (entries[0].isIntersecting) {
      const s = document.createElement("script");
      s.src = "https://cdn.echothread.io/widget.js";
      s.defer = true;
      document.body.appendChild(s);
      observer.disconnect();
    }
  }, { rootMargin: "200px" });
  observer.observe(section);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the widget script never loads for readers who don't scroll past the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;The widget identifies threads by page URL automatically. Works with content collections, MDX, static or SSR builds.&lt;/p&gt;

&lt;p&gt;Full guide with troubleshooting: &lt;a href="https://echothread.io/docs/guides/astro" rel="noopener noreferrer"&gt;echothread.io/docs/guides/astro&lt;/a&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add Comments to a Next.js Site (App Router and Pages Router)</title>
      <dc:creator>Yoni Ryabinski</dc:creator>
      <pubDate>Thu, 09 Apr 2026 00:18:05 +0000</pubDate>
      <link>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-nextjs-site-app-router-and-pages-router-484j</link>
      <guid>https://forem.com/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-nextjs-site-app-router-and-pages-router-484j</guid>
      <description>&lt;p&gt;Next.js gives you so many ways to render a page that the answer to "how do I add comments" depends on which version you're on and which architecture you've chosen. Here's a single approach that works for both the App Router and the Pages Router: a small client component that wraps the &lt;a href="https://echothread.io/" rel="noopener noreferrer"&gt;EchoThread&lt;/a&gt; widget (privacy-first, no ads, under 15 KB gzipped, free during beta).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get an API key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://echothread.io/register" rel="noopener noreferrer"&gt;echothread.io&lt;/a&gt;, add your domain, copy the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add to &lt;code&gt;.env.local&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;NEXT_PUBLIC_ECHOTHREAD_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix is mandatory for client-side access.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create &lt;code&gt;components/EchoThread.tsx&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EchoThread&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_ECHOTHREAD_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"echothread-wrapper"&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4rem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2rem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"echothread"&lt;/span&gt; &lt;span class="na"&gt;data-api-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data-theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.echothread.io/widget.js"&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazyOnload"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;lazyOnload&lt;/code&gt; strategy ensures the widget never blocks LCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use it in your post page
&lt;/h2&gt;

&lt;p&gt;App Router (&lt;code&gt;app/blog/[slug]/page.tsx&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;EchoThread&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/EchoThread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EchoThread&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pages Router is essentially identical — same component, just imported into your &lt;code&gt;pages/blog/[slug].tsx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle soft navigation
&lt;/h2&gt;

&lt;p&gt;Next.js does client-side navigation between routes. To make sure the widget refreshes when readers click between posts, force a remount with a &lt;code&gt;key&lt;/code&gt; prop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;usePathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;EchoThread&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/EchoThread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Wrapper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EchoThread&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Pages Router, use &lt;code&gt;useRouter().asPath&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;The widget identifies threads by page URL automatically. Server components stay server components — only the comment island hydrates.&lt;/p&gt;

&lt;p&gt;Full guide with troubleshooting and CSP notes: &lt;a href="https://echothread.io/docs/guides/nextjs" rel="noopener noreferrer"&gt;echothread.io/docs/guides/nextjs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
