<?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>DEV Community: Yoni Ryabinski</title>
    <description>The latest articles on DEV Community by Yoni Ryabinski (@yoni_ryabinski_da05ba8c9c).</description>
    <link>https://dev.to/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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868711%2F0b764085-4f53-41da-ba88-2dd032a15ff5.png</url>
      <title>DEV Community: Yoni Ryabinski</title>
      <link>https://dev.to/yoni_ryabinski_da05ba8c9c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yoni_ryabinski_da05ba8c9c"/>
    <language>en</language>
    <item>
      <title>Why a local SMTP catcher is not an email integration test</title>
      <dc:creator>Yoni Ryabinski</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:47:54 +0000</pubDate>
      <link>https://dev.to/yoni_ryabinski_da05ba8c9c/why-a-local-smtp-catcher-is-not-an-email-integration-test-3cn1</link>
      <guid>https://dev.to/yoni_ryabinski_da05ba8c9c/why-a-local-smtp-catcher-is-not-an-email-integration-test-3cn1</guid>
      <description>&lt;p&gt;Your Playwright test fills the signup form. The app sends the confirmation email. Mailpit shows the message with the right subject and link. The test passes.&lt;/p&gt;

&lt;p&gt;Then staging sends the same email to a real address and nothing arrives.&lt;/p&gt;

&lt;p&gt;The local catcher did its job. It proved the app opened an SMTP connection, built a MIME message, and rendered the template. It did not prove the message could reach an inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the catcher checks
&lt;/h2&gt;

&lt;p&gt;Mailpit, MailHog, and similar tools are useful. They are fast, free, and easy to run in CI. They catch template errors, missing links, broken HTML, and the wrong recipient in the test payload. Keep them.&lt;/p&gt;

&lt;p&gt;The problem starts when the local catcher is the only email test. It sits inside your network, so it never exercises the parts that decide whether a real message arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cannot see
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What the local catcher shows&lt;/th&gt;
&lt;th&gt;What the real inbox shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS or MX is wrong&lt;/td&gt;
&lt;td&gt;The message is delivered locally&lt;/td&gt;
&lt;td&gt;The message bounces or disappears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM key was rotated&lt;/td&gt;
&lt;td&gt;The message is signed with the old key&lt;/td&gt;
&lt;td&gt;The message fails authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPF or DMARC is misaligned&lt;/td&gt;
&lt;td&gt;The message is accepted&lt;/td&gt;
&lt;td&gt;The message is filtered or rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The provider is sandboxed&lt;/td&gt;
&lt;td&gt;The message is delivered&lt;/td&gt;
&lt;td&gt;Only allowlisted recipients receive it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spam filtering rewrites a link&lt;/td&gt;
&lt;td&gt;The link is intact&lt;/td&gt;
&lt;td&gt;The link points somewhere else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The recipient is over quota&lt;/td&gt;
&lt;td&gt;The message is delivered&lt;/td&gt;
&lt;td&gt;The message is deferred or dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of those failures need a code change to reproduce. They need a real address outside your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: one inbox per test run
&lt;/h2&gt;

&lt;p&gt;Create a real inbox for the test, use its address in the form, wait for the message, assert its contents, then delete the inbox. The inbox can be a hosted service or a small internal service on your own domain. The test shape is the same.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inboxApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type="submit"]&lt;/span&gt;&lt;span class="dl"&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/confirm/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code assumes a small helper; the shape is the point, not the package. The helper is the part you build or buy. The assertions are the part that catches bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The recipient is the exact per-run address, not a shared inbox.&lt;/li&gt;
&lt;li&gt;The sender domain is yours.&lt;/li&gt;
&lt;li&gt;SPF, DKIM, and DMARC pass.&lt;/li&gt;
&lt;li&gt;The link host is on an allowlist. Never follow arbitrary links from email.&lt;/li&gt;
&lt;li&gt;The OTP format and expiry match the product.&lt;/li&gt;
&lt;li&gt;The message contains no real user data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few operational rules keep the test reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One inbox per worker. A shared inbox gives parallel tests the same message.&lt;/li&gt;
&lt;li&gt;Set a TTL and delete the inbox in teardown.&lt;/li&gt;
&lt;li&gt;Log the message id, not the body.&lt;/li&gt;
&lt;li&gt;Do not use a shared Gmail account. MFA, rate limits, and someone reading the inbox will break the test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep the fast test
&lt;/h2&gt;

&lt;p&gt;Run the local catcher on every commit. Run one real-inbox test on the staging deploy or nightly. The first catches template mistakes in seconds. The second catches the failures that only appear after the message leaves your network.&lt;/p&gt;

&lt;p&gt;Replacing Mailpit with a real inbox makes the suite slower and more expensive. Replacing the real-inbox test with Mailpit makes the suite confident about the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try one this week
&lt;/h2&gt;

&lt;p&gt;If your suite only asserts against Mailpit, add one real-inbox test. Pick the signup email or the password reset, because those are the flows users notice first. Run it against staging once. If it passes, keep it in the nightly suite. If it fails, you found a bug your local tests could not see.&lt;/p&gt;




&lt;p&gt;I work on Emcognito. We are testing a hosted version of this: a real address, an HTTP read, and a signed webhook. It is a waitlist, not a live product. If you run email tests, the page asks what you would pay: &lt;a href="https://emcognito.com/test-inbox?src=devto" rel="noopener noreferrer"&gt;https://emcognito.com/test-inbox?src=devto&lt;/a&gt;. No card, and the article works without it.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>email</category>
      <category>devops</category>
    </item>
    <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://dev.to/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-eleventy-site-with-a-custom-shortcode-51c7</link>
      <guid>https://dev.to/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://dev.to/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-hugo-site-without-a-backend-2pn</link>
      <guid>https://dev.to/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://dev.to/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-an-astro-site-lazy-loaded-zero-js-by-default-hba</link>
      <guid>https://dev.to/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://dev.to/yoni_ryabinski_da05ba8c9c/how-to-add-comments-to-a-nextjs-site-app-router-and-pages-router-484j</link>
      <guid>https://dev.to/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>
