<?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: FADI MAMAR</title>
    <description>The latest articles on DEV Community by FADI MAMAR (@mamar).</description>
    <link>https://dev.to/mamar</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%2F3943123%2Ff0ee5b91-eb98-4658-b808-c6c9661007e9.png</url>
      <title>DEV Community: FADI MAMAR</title>
      <link>https://dev.to/mamar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mamar"/>
    <language>en</language>
    <item>
      <title>I Built a Dynamic llms.txt for Next.js. Then Google Said Don't Bother.</title>
      <dc:creator>FADI MAMAR</dc:creator>
      <pubDate>Thu, 21 May 2026 02:32:17 +0000</pubDate>
      <link>https://dev.to/konstruction/i-built-a-dynamic-llmstxt-for-nextjs-then-google-said-dont-bother-4bjd</link>
      <guid>https://dev.to/konstruction/i-built-a-dynamic-llmstxt-for-nextjs-then-google-said-dont-bother-4bjd</guid>
      <description>&lt;p&gt;Two weeks ago I shipped a dynamic &lt;code&gt;llms.txt&lt;/code&gt; and &lt;code&gt;llms-full.txt&lt;/code&gt; for a Next.js 16 site I run. Hourly revalidation, pulls from the same content sources as the sitemap, auto-categorizes by URL pattern, returns proper &lt;code&gt;text/plain&lt;/code&gt;. Took about 40 minutes to build.&lt;/p&gt;

&lt;p&gt;Last week Google published their AI Search optimization guide.&lt;/p&gt;

&lt;p&gt;The relevant line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't create llms.txt files and other "special" markup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So now I have a dynamic llms.txt that the search engine I most care about explicitly says not to bother with.&lt;/p&gt;

&lt;p&gt;I'm keeping it anyway. Here's why, and here's the code in case you want to do the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llms.txt is supposed to do
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;llms.txt&lt;/code&gt; spec was proposed in 2024 (by Jeremy Howard at Answer.AI) as a way for sites to give AI assistants a condensed, machine-readable summary of the site's content. Two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/llms.txt&lt;/code&gt; — short, table-of-contents style. Top-level pages, brief descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/llms-full.txt&lt;/code&gt; — long-form. The actual content for AI ingestion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pitch: LLMs that crawl your site to answer user questions get a curated index instead of having to crawl every page.&lt;/p&gt;

&lt;p&gt;In practice, it never became a real standard. Anthropic, OpenAI, and Perplexity have never officially committed to reading these files. Google has now publicly said it doesn't either.&lt;/p&gt;

&lt;p&gt;But several smaller AI engines and developer-focused indexers do consume llms.txt when present. And the cost to maintain a properly designed one is approximately zero. So the cost/benefit math is interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  My implementation in Next.js 16 App Router
&lt;/h2&gt;

&lt;p&gt;Three files. Total: about 90 lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: a renderer that pulls content from your CMS
&lt;/h3&gt;

&lt;p&gt;I keep mine in &lt;code&gt;lib/llms.ts&lt;/code&gt;. It reads from Sanity, but the same pattern works with anything: Markdown files, MDX, a database, a JSON config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/llms.ts&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;getAllBlogPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getAllResources&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="s1"&gt;@/lib/sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resourceCategory&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comparison&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;glossary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;general&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;building-permit-guide-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vs-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comparison&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &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="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;what-is-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;glossary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;general&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderLlmsTxt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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;posts&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;getAllBlogPosts&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;resources&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;getAllResources&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;lines&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;# Example Company&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt; Toronto-based contractor specializing in framing, drywall, and insulation.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;## Core Pages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;- [Home](https://example.com)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;- [Services](https://example.com/services)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;- [About](https://example.com/about)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;- [Contact](https://example.com/contact-us)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&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;permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resourceCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permit&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;comparisons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resourceCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comparison&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;glossary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resourceCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;glossary&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;## Building Permit Guides&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resources/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comparisons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;## Comparison Pages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comparisons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resources/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;## Glossary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resources/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;## Blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &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;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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="k"&gt;export&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;renderLlmsFullTxt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Same shape as renderLlmsTxt, but inline the actual body content per&lt;/span&gt;
  &lt;span class="c1"&gt;// resource/post. Pull body from your CMS, convert to plaintext, append&lt;/span&gt;
  &lt;span class="c1"&gt;// under each section header.&lt;/span&gt;
  &lt;span class="c1"&gt;// omitted for brevity&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The category split is my preference. The spec is loose. What matters is that a machine reading the file gets a clean hierarchical view of your site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: the route handlers
&lt;/h3&gt;

&lt;p&gt;Next.js 16 App Router treats files inside &lt;code&gt;/app&lt;/code&gt; as routes. Drop a &lt;code&gt;route.ts&lt;/code&gt; inside any folder named for your URL path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/llms.txt/route.ts&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;renderLlmsTxt&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="s1"&gt;@/lib/llms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour cache&lt;/span&gt;

&lt;span class="k"&gt;export&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;GET&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;body&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;renderLlmsTxt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain; charset=utf-8&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/llms-full.txt/route.ts&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;renderLlmsFullTxt&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="s1"&gt;@/lib/llms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;

&lt;span class="k"&gt;export&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;GET&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;body&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;renderLlmsFullTxt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain; charset=utf-8&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;p&gt;The &lt;code&gt;revalidate = 3600&lt;/code&gt; line is the key. It tells Vercel/Next.js to serve a cached version for an hour, then regenerate on the next request. If you publish 10 blog posts a day, the file picks them up within 60 minutes. If you publish once a week, it picks them up within an hour of publishing. No build step required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: deploy and verify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://example.com/llms.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should return your generated markdown-flavored text in seconds.&lt;/p&gt;

&lt;p&gt;That's it. Total code: about 90 lines. Total time: 40 minutes if you've used Next.js before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then Google said don't bother
&lt;/h2&gt;

&lt;p&gt;Google's AI optimization guide (published this month) is explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't create llms.txt files and other "special" markup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reasoning, per Google: their AI features (AI Overviews, AI Mode) ingest the same content humans see. They crawl pages, parse the rendered HTML, and synthesize. They don't read llms.txt. Creating one is "unnecessary effort."&lt;/p&gt;

&lt;p&gt;That's an honest position. Google isn't penalizing you for having an llms.txt. They're telling you it's wasted work from their perspective.&lt;/p&gt;

&lt;p&gt;But Google isn't the only AI engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm keeping mine anyway
&lt;/h2&gt;

&lt;p&gt;Four reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The maintenance cost is zero
&lt;/h3&gt;

&lt;p&gt;Because the file is dynamic and pulls from the same content source as the rest of the site, it updates itself. There's no separate workflow to maintain, no monthly chore, no risk of going stale. The code sat in the repo for two weeks with zero attention required. The next time I add a blog post or resource page, the file picks it up within an hour.&lt;/p&gt;

&lt;p&gt;If maintenance cost were measured in minutes per month, llms.txt would be at zero. There's nothing to abandon.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Not every AI engine has spoken
&lt;/h3&gt;

&lt;p&gt;Perplexity, ChatGPT, Claude, and several smaller AI search products have not officially said whether they read llms.txt. Some open-source RAG indexers do. Some agent frameworks default to checking for it.&lt;/p&gt;

&lt;p&gt;If even one of these tools reads my llms.txt and produces a citation that a human user clicks, the file paid for itself. The opportunity cost of having it is approximately the bytes Vercel serves on the rare requests that hit the endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It functions as a structured fallback
&lt;/h3&gt;

&lt;p&gt;The llms.txt is one of the cleanest, most human-readable summaries of my site that exists anywhere. When someone asks me for an overview of my site's content, I can paste the llms.txt into a chat and have an instant brief. It's a free side effect of building it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Building it was a useful exercise
&lt;/h3&gt;

&lt;p&gt;Going through the exercise of categorizing every page, deciding what's important, and giving each section a clean summary is exactly the kind of content audit most sites should do anyway. The output (the llms.txt file) is the artifact. The thinking is the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to skip it
&lt;/h2&gt;

&lt;p&gt;If your site is purely commerce, your content is highly dynamic (millions of product pages), or your content lives mostly behind login, llms.txt has less value. AI engines aren't going to ingest your product catalog from a flat text file.&lt;/p&gt;

&lt;p&gt;If you're a content site, a docs site, a portfolio, an editorial publication, or anything where the page-level content matters and the URL structure has meaning, llms.txt is worth the 40 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working example
&lt;/h2&gt;

&lt;p&gt;You can see the live output at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://konstruction.ca/llms.txt" rel="noopener noreferrer"&gt;https://konstruction.ca/llms.txt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://konstruction.ca/llms-full.txt" rel="noopener noreferrer"&gt;https://konstruction.ca/llms-full.txt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both regenerate every hour. Both pull from the same Sanity CMS that drives the rest of the site. Total runtime memory cost: indistinguishable from zero. Pageviews: low but non-zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger point
&lt;/h2&gt;

&lt;p&gt;Google publishing guidance that says "don't bother with X" is not the same as Google penalizing X. Sometimes Google is telling you the optimal allocation of your effort. Sometimes you should agree. Sometimes you should keep doing X because the cost is zero and the upside, while small, is nonzero.&lt;/p&gt;

&lt;p&gt;llms.txt is in the second bucket. Build it once, let it run, ignore the file for six months, see what happens.&lt;/p&gt;

&lt;p&gt;If in 18 months Google publishes "we now use llms.txt to inform AI Overviews," you'll be glad you left it running. If they don't, the file cost you 40 minutes and approximately zero ongoing attention.&lt;/p&gt;

&lt;p&gt;The optimal play with low-cost, low-confidence bets is to make a lot of them and let outcomes resolve themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://konstruction.ca" rel="noopener noreferrer"&gt;Build the llms.txt.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Discord.js gotchas that cost me a week each (so they don't have to cost you one)</title>
      <dc:creator>FADI MAMAR</dc:creator>
      <pubDate>Thu, 21 May 2026 02:15:25 +0000</pubDate>
      <link>https://dev.to/vibebot/the-discordjs-gotchas-that-cost-me-a-week-each-so-they-dont-have-to-cost-you-one-2ic7</link>
      <guid>https://dev.to/vibebot/the-discordjs-gotchas-that-cost-me-a-week-each-so-they-dont-have-to-cost-you-one-2ic7</guid>
      <description>&lt;p&gt;I run an &lt;a href="https://vibebot.gg" rel="noopener noreferrer"&gt;AI Discord bot builder&lt;/a&gt; that's deployed 500+ live bots over the last year. The service takes a plain-English description, generates discord.js code, and ships the container to production in under 30 seconds.&lt;/p&gt;

&lt;p&gt;That sounds clean. It is not clean.&lt;/p&gt;

&lt;p&gt;Most of what I learned came from logs at 3am after someone's bot stopped responding to messages, or a community went silent because of an undocumented Discord behavior. Here's the list I wish I'd had on day one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing Permissions (50013) is usually about role position, not permissions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first time I hit this I spent two hours auditing the bot's permission integer. It was correct. The bot still couldn't kick anyone.&lt;/p&gt;

&lt;p&gt;The thing nobody puts in big letters: discord.js permissions are gated by role hierarchy. A bot can have the KickMembers permission and still fail to kick anyone whose top role sits above the bot's top role.&lt;/p&gt;

&lt;p&gt;client.on('interactionCreate', async (interaction) =&amp;gt; {&lt;br&gt;
if (!interaction.isChatInputCommand()) return;&lt;br&gt;
if (interaction.commandName !== 'kick') return;&lt;/p&gt;

&lt;p&gt;const target = interaction.options.getMember('user');&lt;br&gt;
const botMember = interaction.guild.members.me;&lt;/p&gt;

&lt;p&gt;if (target.roles.highest.position &amp;gt;= botMember.roles.highest.position) {&lt;br&gt;
      return interaction.reply({&lt;br&gt;
        content: "I can't kick that user — their role is at or above mine.",&lt;br&gt;
        ephemeral: true,&lt;br&gt;
      });&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await target.kick(interaction.options.getString('reason') ?? 'No reason');
await interaction.reply(`Kicked ${target.user.tag}.`);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;Fix in Discord: drag the bot's role above the roles it needs to manage in Server Settings → Roles. This is the single most common reason a "working" bot doesn't work.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unknown Interaction (10062) is a 3-second race&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Slash commands give you exactly 3 seconds to respond. If you call any awaited DB query, third-party API, or LLM before.reply(), you'll lose that race.&lt;/p&gt;

&lt;p&gt;Wrong:&lt;br&gt;
  const result = await callLLM(prompt);   // 2.4s&lt;br&gt;
  await interaction.reply(result);        // 10062&lt;/p&gt;

&lt;p&gt;Right:&lt;br&gt;
  await interaction.deferReply();         // buys you 15 minutes&lt;br&gt;
  const result = await callLLM(prompt);&lt;br&gt;
  await interaction.editReply(result);&lt;/p&gt;

&lt;p&gt;The deferred reply pattern is one of those things you need to internalize once and then never get bit by again.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Intent flags are silent killers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one is brutal because there's no error. Your bot connects, shows as online, registers commands, and never reads a message.&lt;/p&gt;

&lt;p&gt;Two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Discord Developer Portal, toggle "Message Content Intent" on.&lt;/li&gt;
&lt;li&gt;In your client config, list it:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;import { Client, GatewayIntentBits } from 'discord.js';&lt;/p&gt;

&lt;p&gt;const client = new Client({&lt;br&gt;
    intents: [&lt;br&gt;
      GatewayIntentBits.Guilds,&lt;br&gt;
      GatewayIntentBits.GuildMessages,&lt;br&gt;
      GatewayIntentBits.MessageContent,   // ← without this, message.content is empty&lt;br&gt;
      GatewayIntentBits.GuildMembers,&lt;br&gt;
    ],&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;If you skip either step, message.content arrives as an empty string and your prefix commands look broken for no apparent&lt;br&gt;
   reason.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reactions and edits on old messages are partial&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If someone reacts to a message your bot didn't observe being created (anything older than the bot's session), the event payload is partial. Touching .content returns null until you fetch.&lt;/p&gt;

&lt;p&gt;client.on('messageReactionAdd', async (reaction, user) =&amp;gt; {&lt;br&gt;
    if (reaction.partial) {&lt;br&gt;
      try { await reaction.fetch(); } catch { return; }&lt;br&gt;
    }&lt;br&gt;
    if (reaction.message.partial) {&lt;br&gt;
      try { await reaction.message.fetch(); } catch { return; }&lt;br&gt;
    }&lt;br&gt;
    // safe to use reaction.message.content now&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;Add the Partials config too:&lt;/p&gt;

&lt;p&gt;import { Client, Partials } from 'discord.js';&lt;br&gt;
  new Client({&lt;br&gt;
    intents: [...],&lt;br&gt;
    partials: [Partials.Message, Partials.Channel, Partials.Reaction],&lt;br&gt;
  });&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ephemeral replies don't behave like normal messages&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can't fetch them with channel.messages.fetch(). You can't add reactions. If you want to update one later, you need the original interaction token, not a message ID:&lt;/p&gt;

&lt;p&gt;await interaction.reply({ content: 'Loading…', ephemeral: true });&lt;br&gt;
  // 10 seconds later&lt;br&gt;
  await interaction.editReply({ content: 'Done.' });&lt;/p&gt;

&lt;p&gt;If you serialize the interaction across processes (queue, webhook handler, etc.), serialize the token, not the message&lt;br&gt;
reference.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Process crashes leave gateway sessions hanging&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your bot crashes hard without closing the websocket, Discord keeps that session alive on their end for a few minutes.&lt;br&gt;
New deploys then count against the 1000/day session start limit, and on cold restarts you'll see a brief window where two instances of your bot post duplicate messages.&lt;/p&gt;

&lt;p&gt;async function shutdown(signal) {&lt;br&gt;
    console.log(&lt;code&gt;[bot] ${signal} received, closing gateway&lt;/code&gt;);&lt;br&gt;
    try { await client.destroy(); } catch {}&lt;br&gt;
    process.exit(0);&lt;br&gt;
  }&lt;br&gt;
  process.on('SIGTERM', () =&amp;gt; shutdown('SIGTERM'));&lt;br&gt;
  process.on('SIGINT',  () =&amp;gt; shutdown('SIGINT'));&lt;/p&gt;

&lt;p&gt;process.on('unhandledRejection', (err) =&amp;gt; {&lt;br&gt;
    console.error('[bot] unhandled rejection:', err);&lt;br&gt;
    // don't crash — log and continue&lt;br&gt;
  });&lt;br&gt;
  process.on('uncaughtException', (err) =&amp;gt; {&lt;br&gt;
    console.error('[bot] uncaught:', err);&lt;br&gt;
    // do crash, but cleanly&lt;br&gt;
    shutdown('uncaughtException');&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;If you're running on a container platform that sends SIGTERM before forcibly killing, this single block saves you a&lt;br&gt;
  class of bugs you'd never spot.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rate limits aren't always 429s&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The REST client emits a rateLimited event for the soft cases (Discord's per-route bucket warnings). If you log it from&lt;br&gt;
  day one, you get a free monitoring signal:&lt;/p&gt;

&lt;p&gt;client.rest.on('rateLimited', (info) =&amp;gt; {&lt;br&gt;
    console.warn('[ratelimit]', {&lt;br&gt;
      route: info.route,&lt;br&gt;
      method: info.method,&lt;br&gt;
      timeToReset: info.timeToReset,&lt;br&gt;
      limit: info.limit,&lt;br&gt;
    });&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;In production these almost always trace back to one bot in a 10k-member server doing something dumb in a loop. They Never show up in your error monitoring otherwise.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The biggest one: most production failures aren't bugs in your code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you run a few hundred bots, the failure distribution shifts. Probably 30% of "the bot is broken" tickets I've seen trace back to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server admin moved roles around&lt;/li&gt;
&lt;li&gt;The user revoked the bot's invite&lt;/li&gt;
&lt;li&gt;The bot was rate-limited because someone scripted 1000 reactions&lt;/li&gt;
&lt;li&gt;Discord shipped a behavior change&lt;/li&gt;
&lt;li&gt;A channel was deleted that the bot was scheduled to post in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your bot needs to log enough that you can prove which of these happened quickly. A single structured log per important event (action attempted, guild ID, user ID, outcome, latency) gets you 80% of the way there.&lt;/p&gt;

&lt;p&gt;I learned most of this the hard way running vibebot.gg, where users describe a Discord bot in English and the system generates and deploys discord.js code for them. The interesting failure mode in that setup is that the LLM-generated code has to handle all eight of these gotchas correctly the first time, because the user never sees the code. The patterns above are exactly what we now bake into every generated bot at the template level.&lt;/p&gt;

&lt;p&gt;If you're building a Discord bot from scratch, copy the snippets above directly. They're worth a week of debugging each.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discord</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
