<?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: techpotions</title>
    <description>The latest articles on DEV Community by techpotions (@techpotions).</description>
    <link>https://dev.to/techpotions</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%2F3972335%2F45f3e402-d27f-44f9-be49-702e873aa2be.png</url>
      <title>DEV Community: techpotions</title>
      <link>https://dev.to/techpotions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techpotions"/>
    <language>en</language>
    <item>
      <title>SearXNG JSON API for LLM Grounding</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Tue, 04 Aug 2026 21:37:00 +0000</pubDate>
      <link>https://dev.to/techpotions/searxng-json-api-for-llm-grounding-2gaa</link>
      <guid>https://dev.to/techpotions/searxng-json-api-for-llm-grounding-2gaa</guid>
      <description>&lt;p&gt;The first time a large language model invented a statistic in my draft about search APIs, I knew it was time to ground the thing against real web results. A self-hosted SearXNG JSON API now sits between the model and the internet for our blog pipeline, replacing a paid :online markup and costing nothing at the volume we run.&lt;/p&gt;

&lt;p&gt;What matters more than free: we designed the integration so a search failure degrades the output gracefully rather than breaking the pipeline. That is the piece most builders skip, and it is the piece that makes self-hosting safe to depend on overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  SearXNG JSON API Endpoint
&lt;/h2&gt;

&lt;p&gt;SearXNG exposes a single &lt;code&gt;/search&lt;/code&gt; endpoint. You tell it you want structured output with &lt;code&gt;format=json&lt;/code&gt;, and it hands back a clean payload instead of a rendered results page. The request below is the exact call our pipeline makes—no middleware, no abstraction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://searxng.techpotions.app/search?format=json&amp;amp;q={query}&amp;amp;categories={categories}&amp;amp;time_range={time_range}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parameters that earn their keep
&lt;/h3&gt;

&lt;p&gt;Not every parameter matters for LLM grounding. These are the three we actually use in production.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The search query—exactly what you would type into a search box&lt;/td&gt;
&lt;td&gt;&lt;code&gt;q=vector databases compared 2025&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;categories&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scopes the engine set. &lt;code&gt;general&lt;/code&gt; hits broad web results; mix with &lt;code&gt;news&lt;/code&gt;, &lt;code&gt;science&lt;/code&gt;, or &lt;code&gt;files&lt;/code&gt; when the query calls for it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;categories=general,news&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Narrows results by recency. Critical when grounding the model against what is ranking &lt;em&gt;right now&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time_range=month&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must be &lt;code&gt;json&lt;/code&gt;. Without it, you get an HTML page you do not want to parse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;format=json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;time_range&lt;/code&gt; accepts &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, or &lt;code&gt;year&lt;/code&gt;. For our blog pipeline, &lt;code&gt;month&lt;/code&gt; is the default. When a query targets a fast-moving topic, we drop to &lt;code&gt;week&lt;/code&gt;. When it is evergreen, &lt;code&gt;year&lt;/code&gt; keeps the signal broad.&lt;/p&gt;

&lt;h3&gt;
  
  
  The raw TypeScript call
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchSearXNG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&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="nx"&gt;timeRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://searxng.techpotions.app/search&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;general&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;time_range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeRange&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;response&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;fetch&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;base&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;params&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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="k"&gt;try&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;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What It Replaced: Paid Search Markup
&lt;/h2&gt;

&lt;p&gt;Before the SearXNG instance, our blog pipeline used OpenRouter's &lt;code&gt;:online&lt;/code&gt; search markup. The model would tag a prompt with &lt;code&gt;:online&lt;/code&gt;, and OpenRouter would inject web results from a paid search backend. It worked. It also cost money on every query and leaked portions of our content roadmap into a third-party search API.&lt;/p&gt;

&lt;p&gt;That privacy piece is not theoretical. Our pipeline surfaces what is currently ranking for a target query so drafts are framed against the real SERP rather than the model's stale training data. The search queries are literally a list of the topics we plan to publish over the next quarter. Keeping those queries inside infrastructure we own matters.&lt;/p&gt;

&lt;p&gt;Self-hosting SearXNG makes the grounding layer free at the volume we run, but the real win is that our content roadmap stays ours. For AI services where the prompts themselves are proprietary, this pattern applies just as directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Resilience Pattern: Degrade, Don't Fail
&lt;/h2&gt;

&lt;p&gt;Search is an enhancement to generation, not a precondition for it. A draft written without fresh sources is slightly worse. A draft that never gets written because a cron job exploded at 3am is an editor staring at an empty CMS in the morning.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;searchSearXNG&lt;/code&gt; wrapper never throws. Three failure modes all converge on the same outcome: an empty array.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-OK response&lt;/strong&gt; — upstream engine timed out, instance is restarting, network blip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network error&lt;/strong&gt; — DNS failure, box is unreachable, TLS expiry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed JSON&lt;/strong&gt; — upstream returned an error page, or a reverse proxy injected something unexpected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every path returns &lt;code&gt;[]&lt;/code&gt;. The caller receives zero results instead of an exception in the stack. If your grounding call can throw, your nightly generation job now has a hard dependency on a service you self-host on hardware you are not watching at 3am.&lt;/p&gt;

&lt;p&gt;This is the decision worth the whole integration. Most AI pipelines wire up a search API and then wrap it in a retry loop, as though a retry will fix an instance that has been down for four hours. Retries are for transient failures. This pattern handles the persistent ones.&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;// No try/catch at the call site. No retry loop. No alerts.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sources&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;searchSearXNG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// sources is always an array. Could have 10 results. Could have 0.&lt;/span&gt;
&lt;span class="c1"&gt;// Build the prompt either way.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groundedPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two Jobs the SearXNG JSON API Handles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. SERP-aware framing
&lt;/h3&gt;

&lt;p&gt;The first call in the pipeline searches the target query and returns what is ranking &lt;em&gt;right now&lt;/em&gt;. That surface shapes the draft outline: which angles the top results take, what headings they use, what questions they answer. The model sees the real competitive landscape instead of guessing from stale weights.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Source grounding
&lt;/h3&gt;

&lt;p&gt;The second call searches for authoritative sources on each factual claim the draft makes. The model receives real URLs and real snippets and weaves them into the prose. This dramatically reduces hallucinated statistics and invented quotes. We wrote about the broader pipeline approach in &lt;a href="https://techpotions.com/lab/stop-ai-blog-pipeline-hallucinations" rel="noopener noreferrer"&gt;stopping AI blog pipeline hallucinations&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Caveat
&lt;/h2&gt;

&lt;p&gt;SearXNG is a metasearch aggregator. It does not crawl the web itself. Each query fans out to upstream engines—Google, Bing, DuckDuckGo, and others depending on configuration—and those engines rate-limit. Some block datacenter IPs. Result quality fluctuates in a way a paid search API with a dedicated crawl index does not.&lt;/p&gt;

&lt;p&gt;You own the uptime. You configure the instance, you watch the logs, you handle the inevitable day an upstream engine changes its response format and breaks result parsing. This is the right trade when grounding is a nice-to-have enhancement to generation. It is the wrong trade when search results &lt;em&gt;are&lt;/em&gt; the product.&lt;/p&gt;

&lt;p&gt;Spend the dollars when every result matters. Self-host when the fallback is a perfectly functional model prompt without web context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting SearXNG Running
&lt;/h2&gt;

&lt;p&gt;SearXNG ships as a Docker image with a single container. Bring a domain, add a Let's Encrypt reverse proxy, and set the &lt;code&gt;SEARXNG_SECRET&lt;/code&gt; environment variable to a random value for encryption. The &lt;a href="https://docs.searxng.org/admin/installation-docker.html" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; cover the compose file.&lt;/p&gt;

&lt;p&gt;Our instance at &lt;code&gt;searxng.techpotions.app&lt;/code&gt; runs behind a Traefik reverse proxy with automatic TLS. The configuration file enables JSON output by default and limits the engine set to the ones that reliably return results from the region our queries target.&lt;/p&gt;

&lt;p&gt;For builders integrating search into AI products, we offer &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;AI consulting and development services&lt;/a&gt; that include grounding-layer architecture. If you are starting from scratch, our &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;project launch framework&lt;/a&gt; covers the patterns we reuse across client builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the SearXNG JSON API?
&lt;/h3&gt;

&lt;p&gt;It is the structured output mode of a self-hosted SearXNG metasearch instance. You set &lt;code&gt;format=json&lt;/code&gt; on the &lt;code&gt;/search&lt;/code&gt; endpoint, and the server returns a JSON object with a &lt;code&gt;results&lt;/code&gt; array containing titles, URLs, snippets, and engine metadata—no HTML parsing required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the SearXNG JSON API free?
&lt;/h3&gt;

&lt;p&gt;Yes. The software is open source. You pay only for the infrastructure you run it on. At modest query volumes, a small VPS handles the load without additional search API costs. The trade is that you own the operational burden: uptime, engine configuration, and the occasional upstream breakage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use SearXNG instead of a paid search API for AI agents?
&lt;/h3&gt;

&lt;p&gt;Two reasons make the self-hosted path compelling. First, search queries stay private on your infrastructure—relevant when those queries reveal your content roadmap or proprietary prompts. Second, the resilience pattern of degrading to an empty result set rather than throwing means a search outage does not fail the generation job. Paid APIs solve the uptime problem; they do not automatically solve the architectural one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the SearXNG JSON API in production?
&lt;/h3&gt;

&lt;p&gt;Yes, with the right failure design. Treat search results as an optional enhancement, not a required input. When results are available, the output is stronger. When the instance is down or upstream engines block, the pipeline runs anyway. That architecture is what makes self-hosting production-safe for overnight automation.&lt;/p&gt;

</description>
      <category>searxng</category>
      <category>aiagents</category>
      <category>llmgrounding</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>When to Turn Off Next.js Image Optimization</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Tue, 04 Aug 2026 21:28:41 +0000</pubDate>
      <link>https://dev.to/techpotions/when-to-turn-off-nextjs-image-optimization-1e7n</link>
      <guid>https://dev.to/techpotions/when-to-turn-off-nextjs-image-optimization-1e7n</guid>
      <description>&lt;p&gt;Turning on &lt;code&gt;next.js images unoptimized&lt;/code&gt; looks like a mistake in a code review—and in our own &lt;code&gt;next.config.ts&lt;/code&gt;, it’s exactly what we did, with a comment so nobody reverts it. Our covers were getting softer on mobile, and the culprit was stacking two lossy encoders. Here’s when that config is correct, when it’s dangerous, and how to document it so the next developer doesn’t “fix” it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;code&gt;next.js images unoptimized&lt;/code&gt; is the correct call
&lt;/h2&gt;

&lt;p&gt;The safe time to set &lt;code&gt;images.unoptimized&lt;/code&gt; is when your upstream source already delivers a well‑sized, modern‑format image. In our case, &lt;a href="https://techpotions.com/lab/payload-cms-nextjs-caching-without-cache-components" rel="noopener noreferrer"&gt;Payload CMS&lt;/a&gt; re‑encodes every upload to WebP once, at upload time. Next.js’s &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component then wanted to run its own lossy transformation on top of that already‑lossy WebP. The result: double encoding that visibly softened photographic covers on mobile.&lt;/p&gt;

&lt;p&gt;Turning the optimizer off let the CMS‑encoded WebP serve as‑is. The covers got sharper immediately. The config that made it happen lives in the repo with a comment that explains the trade‑off:&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;// next.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;unoptimized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Payload already encodes to webp at upload;&lt;/span&gt;
                     &lt;span class="c1"&gt;// Next's optimizer was re-encoding and softening images.&lt;/span&gt;
                     &lt;span class="c1"&gt;// Keep off.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broader rule is simple: &lt;strong&gt;image optimization is not additive.&lt;/strong&gt; Each lossy pass compounds the artifacts of the previous encode. If something upstream already produced a compressed, correctly sized image in a modern format (WebP, AVIF), a second optimizer is not making it better—it’s re‑encoding the artifacts of the first encode. Know how many lossy passes your pipeline runs before you let Next.js add another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you must keep Next.js image optimization enabled
&lt;/h2&gt;

&lt;p&gt;Keep Next.js’s optimizer on when you are serving &lt;strong&gt;original‑quality uploads&lt;/strong&gt;—untouched JPEGs, PNGs, or even large TIFFs that come straight from a user’s camera or a design tool. Those files are almost never ready for the web. The optimizer generates responsive &lt;code&gt;srcset&lt;/code&gt; variants across multiple viewport widths, converts to modern formats when the browser supports them, and compresses the output—all without you building a media pipeline.&lt;/p&gt;

&lt;p&gt;You also need the default optimizer when your source images are &lt;strong&gt;not already correctly sized&lt;/strong&gt;. If your CMS just stores whatever the editor uploaded, turning &lt;code&gt;unoptimized&lt;/code&gt; on means you’ll be serving 4000‑pixel‑wide hero shots in a 300‑pixel card. That’s a performance disaster.&lt;/p&gt;

&lt;p&gt;Finally, remember that &lt;code&gt;images.unoptimized&lt;/code&gt; is a global flag. Once you flip it, &lt;strong&gt;you assume responsibility for dimensions, format, and art direction&lt;/strong&gt;. If your CMS doesn’t handle that, you’ve simply removed optimization rather than avoided duplication. A naive reading of this article will make somebody’s site worse—make sure your pipeline is actually doing the job before you reach for the off switch.&lt;/p&gt;

&lt;p&gt;On Vercel, image optimization is a billed transformation. Turning it off where it adds nothing removes a cost line as well as a quality problem. That’s a nice side effect, but quality was our primary driver. Don’t let the cost tail wag the quality dog.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to configure &lt;code&gt;next.js images unoptimized&lt;/code&gt; (and why it needs a comment)
&lt;/h2&gt;

&lt;p&gt;The configuration itself is a single key in &lt;code&gt;next.config.js&lt;/code&gt; or &lt;code&gt;next.config.ts&lt;/code&gt;:&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="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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;unoptimized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;That’s it. But without context, it reads like a lazy shortcut. Any developer—or a future you—will see it and wonder if it was left over from a debug session. A comment transforms it from a potential bug into a documented decision. Ours spells out the CMS workflow, the double‑encoding problem, and the quality improvement. If you’re adopting this pattern, steal that format.&lt;/p&gt;

&lt;p&gt;If you need more granular control (e.g., bypassing optimization only for images from a specific CDN), you’ll have to build a custom loader. The global flag is intentionally blunt. For most sites where the CMS already handles the heavy lifting, that bluntness is a feature—it keeps the pipeline simple and predictable.&lt;/p&gt;

&lt;p&gt;*&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>imageoptimization</category>
      <category>webperf</category>
      <category>payloadcms</category>
    </item>
    <item>
      <title>AI Automation Agencies: Real vs Repackaged</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Mon, 03 Aug 2026 18:02:02 +0000</pubDate>
      <link>https://dev.to/techpotions/ai-automation-agencies-real-vs-repackaged-10l2</link>
      <guid>https://dev.to/techpotions/ai-automation-agencies-real-vs-repackaged-10l2</guid>
      <description>&lt;p&gt;We build AI automation at techpotions, so I’m biased. The only way this article earns your trust is by being so specific that even an agency looking for shortcuts couldn’t replicate it. If you’re evaluating &lt;strong&gt;ai automation agencies&lt;/strong&gt; today, it’s brutally hard to tell the operators who’ve fought real production fires from the repackagers who’ve only ever run demo workflows. Below are the questions we ask ourselves — the uncomfortable ones — and the answers that separate the real from the repackaged.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to separate genuine AI automation agencies from resellers
&lt;/h2&gt;

&lt;p&gt;The quickest test: ask what the agency itself runs and what broke. A shop that has never scraped its knees has no stories. At techpotions, our publishing pipeline is a Payload &lt;code&gt;post.published&lt;/code&gt; webhook into &lt;a href="https://techpotions.com/solutions/n8n-automation-agency" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;. The workflow fans a new article out to dev.to, Hashnode, and LinkedIn, sets canonical URLs back to our site, then writes the resulting platform URLs back into the Payload database. It broke in production — syndicated copies went out with relative links, so &lt;code&gt;/services/ai&lt;/code&gt; resolved to &lt;code&gt;dev.to/services/ai&lt;/code&gt; and died. We fixed it by absolutising every link before posting. An agency that can’t tell you something similar has only ever watched, not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask where the human approval gate lives
&lt;/h2&gt;

&lt;p&gt;Ethical &lt;strong&gt;ai automation agencies&lt;/strong&gt; don’t remove the human from decisions that affect your brand. In our pipeline, generation and review stay inside the application — a person approves every draft. Only after approval does n8n cross-post. That architecture lets us add or remove platforms with zero code change, but the approval gate is immutable. If an agency advertises fully automated end-to-end social or content, they’re selling you unreviewed output. The question isn’t whether they “have a review step,” but &lt;em&gt;where&lt;/em&gt; it sits. Ours is in the application, not in the automation. That’s what protects you from the drift that comes when no human sees the copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real tell: what an AI automation agency refuses to automate
&lt;/h2&gt;

&lt;p&gt;This cuts hard, even against us. We killed our own trending-news content mode entirely. It churned out posts that might as well have been spun — undifferentiated, zero proof points. We rebuilt the generator so it &lt;em&gt;writes nothing&lt;/em&gt; unless a topic carries a real, verifiable detail the reader can check. That means it sometimes outputs silence. An agency that says “everything is automatable” hasn’t run anything long enough to hit the limits. When you interview &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;ai automation agencies&lt;/a&gt;, ask what they’ve deliberately removed from their own automation. If the answer is “nothing,” keep looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syndication doesn’t buy you backlinks — and honest agencies say so
&lt;/h2&gt;

&lt;p&gt;Anyone packaging syndication as link building is either ignorant or dishonest. Syndicated links on dev.to, Hashnode, LinkedIn, and similar platforms are &lt;code&gt;nofollow&lt;/code&gt;. They pass zero PageRank. What syndication actually does is buy referral traffic, brand reach, and visibility in AI-generated answers — all valuable, but nothing to do with backlinks. Watch for agencies that conflate the two. If they lead with “do-follow” or imply SEO juice, they’re repackaging someone else’s playbook without understanding the &lt;code&gt;nofollow&lt;/code&gt; reality. Read more about how we think about &lt;a href="https://techpotions.com/lab/how-to-build-an-ai-agent-with-n8n" rel="noopener noreferrer"&gt;building AI agents with n8n&lt;/a&gt; to see where real automation value lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in your next conversation
&lt;/h2&gt;

&lt;p&gt;When you sit down with a potential automation partner, here’s a simple scorecard:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What signals a real operator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;“Show me an internal automation that broke.”&lt;/td&gt;
&lt;td&gt;Specific story with the fix, not “We’ve never had a failure.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Where is the human approval gate?”&lt;/td&gt;
&lt;td&gt;Inside the application, before any automated distribution (ours: &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;blog → review → n8n cross-post&lt;/a&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“What will you refuse to automate for me?”&lt;/td&gt;
&lt;td&gt;Clear boundaries, like no-touch content generation without a proof point.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Does my syndication plan build backlinks?”&lt;/td&gt;
&lt;td&gt;“No — syndication platforms use nofollow. You’ll get traffic, brand, and AI visibility.”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AI automation agencies&lt;/strong&gt; that pass these four questions have earned the right to talk about their stack. The rest are selling packaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How can I tell if an AI automation agency actually runs its own workflows?
&lt;/h3&gt;

&lt;p&gt;A genuine operator will readily describe their own internal automations — including what broke and why. If they can’t share a specific failure story that required a fix, they’re likely running demos, not production systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where should human review sit in an automation pipeline?
&lt;/h3&gt;

&lt;p&gt;Ethical agencies insert a human approval gate before any automated action goes public. Ours lives inside the application: every draft is reviewed before n8n cross-posts it. End-to-end automation with no human review means unreviewed output is heading straight to your audience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do syndicated blog posts help with SEO backlinks?
&lt;/h3&gt;

&lt;p&gt;Syndicated links on platforms like dev.to, Hashnode, and LinkedIn are nofollow. They won’t pass PageRank. Syndication buys referral traffic, brand reach, and visibility in AI-generated answers — not backlinks for SEO.&lt;/p&gt;

</description>
      <category>aiautomation</category>
      <category>workflowautomation</category>
      <category>buyerguide</category>
      <category>n8n</category>
    </item>
    <item>
      <title>What Agentic AI Development Services Actually Ship</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Mon, 03 Aug 2026 18:01:37 +0000</pubDate>
      <link>https://dev.to/techpotions/what-agentic-ai-development-services-actually-ship-4op1</link>
      <guid>https://dev.to/techpotions/what-agentic-ai-development-services-actually-ship-4op1</guid>
      <description>&lt;p&gt;When technical buyers go shopping for &lt;strong&gt;agentic AI development services&lt;/strong&gt;, they usually picture the agent—the autonomous loop that plans, tools around, and completes a task. That is the glamorous 10%. The other 90% is everything that stops the loop from shipping a regression, returning unparsable sludge, or confidently breaking something expensive. Those ungroomed parts are what a serious quote is actually pricing.&lt;/p&gt;

&lt;p&gt;At techpotions, we learned this the hard way, shipping agentic systems for recruitment screening, voice products, and content pipelines. Here is the deliverable list buyers never see quoted—and why each line item is real work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Agentic AI Development Services Actually Deliver
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. An Eval Suite That Ships Before the Agent
&lt;/h3&gt;

&lt;p&gt;An agent without an eval set is a chemistry set without a scale. You cannot call a prompt change an improvement if you have no reproducible way to measure it against the last version. You will ship a regression, and you will not know until a customer tells you.&lt;/p&gt;

&lt;p&gt;We ship the eval suite before launch. For one product, we built 380 golden cases; for another, 240. Every prompt edit runs the full suite. The cases cover happy paths, edge cases, adversarial inputs, and the product’s specific error modes (e.g., misreading years of experience, confusing similar company names).&lt;/p&gt;

&lt;p&gt;Building those cases is the labour: extracting them from raw logs, neutralizing live data, writing the grade rubric for each one, and wiring the harness into CI so it blocks a deploy. This line item alone is often the heaviest in an agentic build, and it is conspicuously absent from most scoping conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A Structured Output Contract, Not Free Text
&lt;/h3&gt;

&lt;p&gt;An agent that returns free text is not integratable. Downstream systems—search indexes, CRMs, dashboards, notification triggers—need a contract. If the agent writes a novel, someone has to parse it, and that someone will hate you.&lt;/p&gt;

&lt;p&gt;We use native &lt;code&gt;json_schema&lt;/code&gt; strict mode wherever the provider supports it, with a &lt;code&gt;json_object&lt;/code&gt; fallback for models that reject strict schemas. This matters because any &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;agentic AI development&lt;/a&gt; stack that fans out across multiple model providers through a gateway cannot assume uniform structured-output support. You code for the weakest link.&lt;/p&gt;

&lt;p&gt;The work here is not just declaring a schema. It is designing the schema so it survives real variability—nullable fields that triple-providers omit, enum sets that stay open, and validation that catches what the model got wrong &lt;em&gt;before&lt;/em&gt; it hits a database constraint. The deliverable is a typed contract, a validator, and a retry loop that asks the model to fix its own broken JSON instead of silently dropping the turn.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A Defined Human Handoff Point
&lt;/h3&gt;

&lt;p&gt;Decide what is expensive to be wrong about and put the human there. This is a design decision, not an afterthought, and it materially changes the system architecture.&lt;/p&gt;

&lt;p&gt;In our recruitment screening product, the agent produces a ranked shortlist with quoted evidence per criterion—transcript snippets for each claim, scored against the job description. It never auto-rejects a candidate. The final “advance / decline” call stays with the recruiter, because a false negative is a breached commitment to the client. You can &lt;a href="https://techpotions.com/work/readyshortlist" rel="noopener noreferrer"&gt;read the full case study here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In our orientation product, we priced human review as a separate tier rather than baking it into the base service. Some organizations want fully automated onboarding flows; others need a human in the loop for compliance-sensitive steps. The architecture supports both without the overhead collapsing into a single, inflated per-seat cost.&lt;/p&gt;

&lt;p&gt;The deliverable is an explicit handoff protocol: the trigger condition, the data packet the human reviews, the UI they review it in, and the timeout/fallback if no one responds. That all has to be built.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Grounding Infrastructure That Never Takes Generation Down
&lt;/h3&gt;

&lt;p&gt;An agent answering from model memory answers from stale memory. Grounding it in fresh data is table stakes, but the implementation detail that separates a demo from a production service is what happens when the grounding layer fails.&lt;/p&gt;

&lt;p&gt;We run a self-hosted SearXNG instance as the grounding layer for our content pipeline. The wrapper is written to never throw: on any failure—timeout, no results, malformed response—it returns an empty result set and logs the event. This is non-negotiable. A grounding search going down must not take generation down with it.&lt;/p&gt;

&lt;p&gt;The deliverable is the retriever API with the circuit breaker, the cache layer that keeps the most-queried documents warm, and the fallback prompt that tells the model “you have no live context, respond conservatively” instead of letting it hallucinate into the gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Observability on the Ops Surface
&lt;/h3&gt;

&lt;p&gt;Someone inside your customer’s organization will need to answer: &lt;em&gt;what did our agent say to this specific person at this specific time?&lt;/em&gt; If the answer is “check the server logs,” you shipped a gap.&lt;/p&gt;

&lt;p&gt;For the voice product we built, the dashboard covers calls, transcriptions, agent interactions, CRM records, and user management. It is a single pane because the people who answer those questions—support leads, compliance officers, product managers—do not live in your observability stack.&lt;/p&gt;

&lt;p&gt;This surface is not a thin skin over telemetry. It requires logging every turn with the full payload (prompt, completion, tool calls, latency), indexing it on the dimensions the business cares about, and building a UI that can replay a conversation without looking like a raw JSON dump. None of that is glamorous. All of it is the difference between “the agent works” and “we can run a business on it.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Model Call Is the Small Part
&lt;/h2&gt;

&lt;p&gt;The dominant framing in the market—and on cloud providers’ landing pages—is that you pick a framework, chain some tools, and deploy. AWS positions agentic AI as something that “&lt;a href="https://aws.amazon.com/ai/agentic-ai/" rel="noopener noreferrer"&gt;removes constraints across how you build, secure, and ship software&lt;/a&gt;.” Google Cloud offers “a flexible and modular framework for developing and deploying AI agents.” These are useful raw ingredients. They are not the meal.&lt;/p&gt;

&lt;p&gt;What is missing from the platform pitches is the work that happens &lt;em&gt;between&lt;/em&gt; the framework and the production line: designing the guard evaluator, building the golden dataset, hardening the output schema, wiring the human fallback, and making the whole thing observable enough to trust. These are the deliverables an &lt;strong&gt;agentic AI development services&lt;/strong&gt; engagement should quote line by line, because they are where the budget actually goes.&lt;/p&gt;

&lt;p&gt;If you are scoping a build, &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;start here&lt;/a&gt; with the checklist no one sends you, or read &lt;a href="https://techpotions.com/lab/how-to-add-evals-to-an-llm-feature" rel="noopener noreferrer"&gt;how we add evals to an LLM feature&lt;/a&gt; for the specific pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What do agentic AI development services actually cost?
&lt;/h3&gt;

&lt;p&gt;We don’t publish day rates or fixed prices because the scope varies dramatically: a single-agent screening tool with a handoff point is a different build from a multi-agent voice product with a compliance dashboard. The cost is driven by the eval suite size, the number of integrations, and the observability surface. &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;Reach out here&lt;/a&gt; with your use case, and we will map it to deliverables.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to build an agentic AI product?
&lt;/h3&gt;

&lt;p&gt;From our experience, a focused agentic product (e.g., a screening agent with a human review tier) takes weeks, not months, to reach a production-grade MVP—&lt;em&gt;if&lt;/em&gt; the scope is tightly defined and the eval suite is built alongside the first prompt experiments. Larger multi-agent systems with custom dashboards and multiple integrations extend the timeline. The blocker is rarely the model logic; it is the grounding, the contracts, and the edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need structured output for my agent?
&lt;/h3&gt;

&lt;p&gt;Almost certainly yes. If your agent is consumed by a human reading text, free-form output can work. But if its decisions feed into another system—a database write, a status change, a notification trigger—you need a schema. Without it, you are pushing the parsing problem downstream, where it will surface as brittle regex, dropped records, and angry integration partners. We ship a structured output contract on every agentic build.&lt;/p&gt;

</description>
      <category>agenticai</category>
      <category>aiagents</category>
      <category>aidevelopment</category>
      <category>productionsystems</category>
    </item>
    <item>
      <title>When Not to Use an AI Agent</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:30:24 +0000</pubDate>
      <link>https://dev.to/techpotions/when-not-to-use-an-ai-agent-po</link>
      <guid>https://dev.to/techpotions/when-not-to-use-an-ai-agent-po</guid>
      <description>&lt;p&gt;When not to use an AI agent isn’t about cost or complexity first — it’s about whether you can name every step from start to finish. We build AI agents for a living, yet the smartest moves in our own stack replaced would‑be agents with plain automation and human judgment. Here are the three times we killed the agent, why, and the decision rule that’ll keep you from over‑engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use an AI Agent
&lt;/h2&gt;

&lt;p&gt;The industry defaults to reaching for an agent the moment work involves text, images, or decisions. But most tasks sold as “agent problems” really fall into two simpler buckets: deterministic workflows where the steps are known, and high‑stakes decisions where getting it wrong is expensive and invisible. In both, adding an LLM that decides its own path doesn’t improve outcomes — it introduces a failure mode you can’t enumerate.&lt;/p&gt;

&lt;p&gt;Our litmus test, refined on our own production systems, is: &lt;strong&gt;use an agent only when the path is genuinely unknown at design time and the cost of a wrong step is recoverable.&lt;/strong&gt; The three cases below show exactly where that line sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Publishing Fan‑Out — When Every Step Is Known, Write a Pipeline, Not an Agent
&lt;/h2&gt;

&lt;p&gt;When we publish a blog post, it gets pushed to dev.to, Hashnode, and LinkedIn — with canonical URLs pointing back to the original, and the resulting social URLs written into our database. This is a webhook into an &lt;a href="https://techpotions.com/lab/how-to-build-an-ai-agent-with-n8n" rel="noopener noreferrer"&gt;n8n workflow&lt;/a&gt; that processes the payload, sends the content to each platform’s API, captures the response, and updates a CMS record.&lt;/p&gt;

&lt;p&gt;There is no model in it. There shouldn’t be. Every single step has exactly one correct outcome. An agent that “decides” which API to call or how to format the payload would only add a way for it to be wrong — a malformed LinkedIn post, a missing canonical tag, a database write that fails silently because the agent guessed a different field name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; If you can write down the sequence — authenticate, post, parse response, write back — you don’t need something that invents the sequence at runtime. Deterministic code is faster, cheaper, and trivially testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: Content Ideation — Constrain the System, Don’t Optimize the Agent
&lt;/h2&gt;

&lt;p&gt;The obvious agentic design for a content generator is a loop: ideate topics, judge them with an LLM, pick the best, and start writing. We built a version close to that and killed it.&lt;/p&gt;

&lt;p&gt;Why? Because it defaulted to trending news. The agent dutifully recycled existing top‑ranking pieces into summaries that added no net‑new signal. It wasn’t “hallucinating” — it was rationally producing the kind of content that already existed, just faster. That’s not what we needed.&lt;/p&gt;

&lt;p&gt;The replacement isn’t smarter; it’s dumber and better. The generator now &lt;strong&gt;reads a human‑curated backlog&lt;/strong&gt; of real proof points and first‑hand notes. It drafts only topics that carry a verifiable insight from our own work. If the backlog is empty, it writes nothing at all. No agent loop, no ranking model — just a constraint that the input must contain a concrete, non‑searchable claim.&lt;/p&gt;

&lt;p&gt;Constraining the system beat improving the agent. When the universe of acceptable inputs is narrow and deterministic, you don’t need an LLM to filter; you need a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: Candidate Screening — Automate the Reading, Not the Judgment
&lt;/h2&gt;

&lt;p&gt;In our candidate screening, a model reads and scores applications. That part is worth automating — turning unstructured text into structured signals saves hours. But the final decision? That stays with a human.&lt;/p&gt;

&lt;p&gt;A false reject here is expensive and invisible: you’ll never know you missed the best hire, and the candidate gets no meaningful feedback. Automating the reading was a clear win; automating the judgment was not. The model produces a score, a human reviews the shortlist, and the human decides.&lt;/p&gt;

&lt;p&gt;This is the third spot where agents break down: &lt;strong&gt;when the cost of a wrong step is asymmetric and hard to detect, keep the human in the loop.&lt;/strong&gt; A model can recommend; it shouldn’t decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Rule: Agent, Deterministic Code, or Human
&lt;/h2&gt;

&lt;p&gt;Distilled from those three production decisions, here’s the heuristic we apply to every new feature:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You can enumerate every step and each step has one correct answer&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Deterministic code&lt;/strong&gt; (webhooks, serverless functions, n8n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The path is ambiguous at design time, but wrong steps are cheap and recoverable&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;AI agent&lt;/strong&gt; (LLM‑driven decision loops)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The path is ambiguous or high‑stakes, and wrong steps are expensive or invisible&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Human judgment&lt;/strong&gt;, optionally supported by deterministic scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When in doubt, ask: “If this step goes wrong, will we notice immediately?” If the answer is no, don’t give it to an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Selling Agents Means Telling Clients “Don’t Build One”
&lt;/h2&gt;

&lt;p&gt;We offer &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;AI consulting and agent development&lt;/a&gt;. The honest version of this business — the one that builds trust and actually works — is sitting down with a client who walks in with three agent ideas and saying, “Number two doesn’t need an agent. We can build it as a plain automation in a couple of days for a fraction of the cost.”&lt;/p&gt;

&lt;p&gt;The best outcome isn’t a flashy autonomous system; it’s a pipeline that runs silently and never breaks. Most of the value we deliver is recognizing when not to use an AI agent, and building the other two ideas as reliable code. If you want to have that conversation about your own stack, &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;start here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the simplest test for when not to use an AI agent?
&lt;/h3&gt;

&lt;p&gt;If you can enumerate every step and there is exactly one correct outcome for each, use deterministic code. If the cost of a wrong step is expensive and hard to detect (e.g., rejecting a great candidate), keep a human in the loop. Only use an agent when the path is genuinely unknown at design time and the cost of a mistake is recoverable.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is an AI agent different from a chatbot, and why does that matter?
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://techpotions.com/lab/ai-agent-vs-chatbot-difference" rel="noopener noreferrer"&gt;chatbot follows a fixed conversation flow; an AI agent decides its own sequence of actions&lt;/a&gt;. Agents introduce uncertainty — exactly what you want to avoid when you already know the right steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use n8n instead of building a full AI agent?
&lt;/h3&gt;

&lt;p&gt;Absolutely. For deterministic workflows like our publishing fan‑out, we &lt;a href="https://techpotions.com/lab/how-to-build-an-ai-agent-with-n8n" rel="noopener noreferrer"&gt;use n8n without any LLM step&lt;/a&gt;. It’s faster, cheaper, and never hallucinates a URL. You don’t need an agent just because a tool supports one.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>deterministicautomation</category>
      <category>n8n</category>
    </item>
    <item>
      <title>Shiki in Next.js App Router: Zero Client JS</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:29:55 +0000</pubDate>
      <link>https://dev.to/techpotions/shiki-in-nextjs-app-router-zero-client-js-17ed</link>
      <guid>https://dev.to/techpotions/shiki-in-nextjs-app-router-zero-client-js-17ed</guid>
      <description>&lt;p&gt;Server-side syntax highlighting with Shiki in the Next.js App Router means you ship exactly zero client‑side JavaScript for code blocks. That’s 0 KB of grammar definitions, 0 KB of theme data, and 0 KB of a highlighter library—browser downloads that often add 50–100 KB before a single line becomes readable. Instead, every code block is rendered into coloured HTML spans on the server, and the browser just paints it. This how‑to walks through the core move, the performance details that make it safe, and the maintenance trap that will bite you if you wire it into a CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shiki + Next.js App Router: Zero KB of Client JS
&lt;/h2&gt;

&lt;p&gt;The takeaway: when you call Shiki’s &lt;code&gt;codeToHtml&lt;/code&gt; inside a Server Component, the visitor’s browser receives ready‑to‑display HTML. No highlighter bundle is ever downloaded. Compare that with a typical client‑side highlighter like Prism or Highlight.js, where the grammar and theme data are part of the bundle the browser must fetch, parse, and execute before code becomes readable. On a slow connection, that delay is visible—a flash of unstyled text or a blank block while the scripts load.&lt;/p&gt;

&lt;p&gt;With Shiki in the App Router, that bundle simply doesn’t exist. The server does the work once at render time, and the output is &lt;code&gt;&amp;lt;span style="color: #..."&amp;gt;&lt;/code&gt; elements that any browser can handle immediately. The performance win is permanent, not just a first‑load trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Move: Highlighting in a Server Component
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;highlight&lt;/code&gt; function that runs &lt;code&gt;codeToHtml&lt;/code&gt; and returns the HTML string. The key is an &lt;strong&gt;allowlist&lt;/strong&gt; of supported languages that fall back to plain text when an unsupported language is requested. Without that fallback, one editor picking an unlisted language will take down the page render.&lt;/p&gt;

&lt;p&gt;Here’s the function we use at techpotions to highlight every code block on our blog:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;codeToHtml&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;shiki&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;ALLOWED_LANGUAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typescript&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;tsx&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;javascript&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;jsx&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;json&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;bash&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;python&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;go&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;rust&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;sql&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;html&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;css&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;yaml&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;markdown&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&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;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&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="nx"&gt;lang&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="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;safeLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_LANGUAGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;lang&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&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="nf"&gt;codeToHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;safeLang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitesse-dark&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call this function inside a Server Component and inject the result with &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. Because the HTML is generated on the server, there’s no hydration mismatch and no client‑side JavaScript involved.&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="c1"&gt;// app/(blog)/[slug]/page.tsx&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;highlight&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/shiki&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;BlogPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;language&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;html&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;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;language&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="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;html&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Approach Scales: Shiki’s Internal Caching
&lt;/h2&gt;

&lt;p&gt;A common worry: “If I highlight 20 code blocks on a page, will I create 20 highlighter instances?” No. Shiki maintains a singleton highlighter internally. It lazily loads each grammar and theme, then caches them. So repeated calls to &lt;code&gt;codeToHtml&lt;/code&gt; within a single render are cheap—you don’t need to hand‑roll a highlighter cache or worry about instantiating one per block.&lt;/p&gt;

&lt;p&gt;This caching is why the allowlist‑and‑fallback pattern works so well. Even if a page has a dozen code blocks in different languages, Shiki only loads the grammars it actually needs, and the fallback to &lt;code&gt;text&lt;/code&gt; costs next to nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Trap: Keeping Language Allowlists in Sync
&lt;/h2&gt;

&lt;p&gt;The practical advice that saves a build: you need an explicit allowlist of supported languages, and it has to stay in sync with whatever your CMS offers as language options. Our allowlist is a 15‑entry set kept in lockstep with the language dropdown on our Payload code block. Anything not in the set falls back to &lt;code&gt;text&lt;/code&gt; rather than throwing. Without the fallback, a single typo or an editor adding a new language in the CMS would break the entire page render.&lt;/p&gt;

&lt;p&gt;If you’re wiring this into a rich‑text editor, there’s a related gotcha that hits you right before this one. The default markdown‑to‑Lexical converter has no transformer for a custom code block, so fenced code has to be split out and hand‑built into the right node type before conversion. That’s a separate problem from highlighting, but it’s the one you’ll hit immediately. (We addressed several Payload Lexical quirks while building this system—including a fix for GFM table rendering that you can read about in our &lt;a href="https://techpotions.com/lab/payload-lexical-gfm-tables-fix" rel="noopener noreferrer"&gt;lab post on Payload Lexical GFM tables&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Zero‑JS Blog with Us
&lt;/h2&gt;

&lt;p&gt;This approach is at the core of the Next.js sites we build at techpotions. By moving highlighting to the server, we keep the client bundle lean and the Critical Path fast. If you want a performant Next.js blog that ships zero unnecessary JavaScript, explore our &lt;a href="https://techpotions.com/services/web" rel="noopener noreferrer"&gt;web development services&lt;/a&gt; or &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;start a project&lt;/a&gt; with us.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happens if an unsupported language is passed to the highlighter?
&lt;/h3&gt;

&lt;p&gt;The highlight function falls back to &lt;code&gt;text&lt;/code&gt;—a built‑in Shiki language that wraps the code in &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt; without any syntax coloring. The page renders normally, the code is readable, and no error is thrown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a different theme or multiple themes?
&lt;/h3&gt;

&lt;p&gt;Shiki works with any theme you supply. You can switch themes by passing a different built‑in theme name or a custom JSON theme object to &lt;code&gt;codeToHtml&lt;/code&gt;. For dark‑mode support, you can render both a light and dark theme on the server and toggle them with CSS, but that re‑introduces a tiny bit of client logic. Many teams simply pick one theme that works well in both modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add a new language to the allowlist?
&lt;/h3&gt;

&lt;p&gt;Install the corresponding language grammar (for example, &lt;code&gt;npm install @shikijs/langs&lt;/code&gt; if you’re using the bundled approach) and add its identifier to the &lt;code&gt;ALLOWED_LANGUAGES&lt;/code&gt; set. Then synchronise the CMS editor’s language dropdown so content authors can select it.&lt;/p&gt;

</description>
      <category>shiki</category>
      <category>nextjs</category>
      <category>approuter</category>
      <category>syntaxhighlighting</category>
    </item>
    <item>
      <title>What the Claude API Actually Costs</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Fri, 31 Jul 2026 20:36:12 +0000</pubDate>
      <link>https://dev.to/techpotions/what-the-claude-api-actually-costs-47d5</link>
      <guid>https://dev.to/techpotions/what-the-claude-api-actually-costs-47d5</guid>
      <description>&lt;p&gt;If you’re estimating the Claude API cost for a production feature, the per-model prices below give you the raw numbers—but the two structural forces that will shape your actual bill are the &lt;strong&gt;output-token multiplier&lt;/strong&gt; and the &lt;strong&gt;expiration of introductory pricing&lt;/strong&gt; on Sonnet 5.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Prices verified July 2026 against Anthropic’s official page; the &lt;a href="https://techpotions.com/tools/llm-api-pricing-calculator" rel="noopener noreferrer"&gt;LLM API pricing calculator&lt;/a&gt; that powers these figures stamps every report with &lt;code&gt;PRICING_AS_OF&lt;/code&gt; so the table can never quietly go stale.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude API Cost: Per-Model Pricing Breakdown
&lt;/h2&gt;

&lt;p&gt;Anthropic’s standard (non-batch) list prices as of July 2026. Every row was validated against &lt;code&gt;platform.claude.com/docs/en/about-claude/pricing&lt;/code&gt; on that date.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input ($/M tokens)&lt;/th&gt;
&lt;th&gt;Output ($/M tokens)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Sonnet 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$3 (intro: $2)&lt;/td&gt;
&lt;td&gt;$15 (intro: $10)&lt;/td&gt;
&lt;td&gt;Introductory pricing ends 31 August 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Opus 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Fable 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things jump out immediately: output tokens cost exactly &lt;strong&gt;5× input tokens&lt;/strong&gt; across the whole line, and Sonnet 5’s promo rate has a hard stop—modeling long-term unit economics on $2/$10 is a budget error waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Output Tokens Drive Your Claude API Bill (5× the Input Cost)
&lt;/h2&gt;

&lt;p&gt;Forget the instinct to trim your system prompt; the real lever is response length. Every Claude model in this generation charges five times more per token that the API &lt;em&gt;writes&lt;/em&gt; than per token it &lt;em&gt;reads&lt;/em&gt;. A verbose output format will cost you far more than a long prompt ever will.&lt;/p&gt;

&lt;p&gt;Take a typical Sonnet 5 request under intro pricing with 1 million input tokens and just half that much output:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputTokens&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 1M tokens&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;   &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 500k tokens&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputPrice&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// $2 per million tokens (intro)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// $10 per million tokens (intro)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt;  &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;inputPrice&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;outputPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// cost = $2 + $5 = $7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the output is half the size, it’s responsible for over 70% of the $7 bill. When the intro rate expires and the same request hits $3/$15, the split becomes $3 + $7.50 = $10.50—still dominated by output.&lt;/p&gt;

&lt;p&gt;If you’re building a Claude-backed feature, &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;our AI development team&lt;/a&gt; spends as much time on response shaping and structured output schemas as on prompt engineering, precisely because that 5× ratio makes output length the budget’s loudest dial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Introductory Price Trap: Sonnet 5’s Promo Rate Expires
&lt;/h2&gt;

&lt;p&gt;Anthropic’s Sonnet 5 introductory pricing—$2/M input, $10/M output—is current until &lt;strong&gt;31 August 2026&lt;/strong&gt;. On 1 September 2026, the standard rate of $3/$15 kicks in.&lt;/p&gt;

&lt;p&gt;That’s a 50% increase on input and a 50% increase on output. Anyone building a product whose unit economics rely on the intro tier needs to bake that step-change into their runway model now. There’s no guarantee of an extension, and the standard rate is the number you’ll be paying for 99% of the product’s lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond List Prices: Prompt Caching and Batch Lower Your Effective Rate
&lt;/h2&gt;

&lt;p&gt;The table above shows the standard, on‑demand tier—the ceiling. In practice, two features can pull your effective Claude API cost well below the list numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching&lt;/strong&gt;: When you re-use a large system prompt or context across many calls, caching can slash the input price for the repeated portion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt;: Submitting jobs asynchronously with longer latency windows often earns a substantial discount. The exact multiplier varies, but it’s enough to turn the list price from “budget assumption” to “worst-case scenario.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key: these are not footnotes. If your workload has any regularity, model your costs &lt;em&gt;with&lt;/em&gt; caching and batch factored in, not the rack rate. The &lt;a href="https://techpotions.com/tools/llm-api-pricing-calculator" rel="noopener noreferrer"&gt;LLM API pricing calculator&lt;/a&gt; lets you toggle those levers on your own token volumes, so you can compare effective rates instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Your Own Usage—Don’t Trust a Worked Example
&lt;/h2&gt;

&lt;p&gt;This article includes one code snippet; your own traffic patterns are the only ones that matter. Visit the &lt;a href="https://techpotions.com/tools/llm-api-pricing-calculator" rel="noopener noreferrer"&gt;LLM API pricing calculator&lt;/a&gt;, enter your expected monthly input and output tokens, and see a dated, per-model breakdown that you can screenshot and share with your team. If you’re ready to architect the feature on the right model from day one, &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;start a project with us&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Note for the publisher: Every number in this article was sourced from Anthropic’s official pricing page on 2026-07-25. Before publication, re-verify all values at &lt;code&gt;platform.claude.com/docs/en/about-claude/pricing&lt;/code&gt; and update the dateline accordingly. A pricing article with a stale table is worse than no article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>pricing</category>
    </item>
    <item>
      <title>Next.js Hydration Mismatch: The SVG Gradient Trap</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:49:00 +0000</pubDate>
      <link>https://dev.to/techpotions/nextjs-hydration-mismatch-the-svg-gradient-trap-46g</link>
      <guid>https://dev.to/techpotions/nextjs-hydration-mismatch-the-svg-gradient-trap-46g</guid>
      <description>&lt;p&gt;If you've ever stared at a &lt;strong&gt;next.js hydration mismatch&lt;/strong&gt; error with no obvious explanation, the culprit might be hiding inside an SVG gradient id. Our own studio hit this exact bug in &lt;code&gt;Wordmark.tsx&lt;/code&gt; and &lt;code&gt;BlobMark.tsx&lt;/code&gt; — a component that looked purely presentational, yet threw the infamous "Hydration failed because the initial UI does not match what was rendered on the server." The fix was a single import.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Next.js Hydration Mismatch Really Means – And Where It Can Hide
&lt;/h2&gt;

&lt;p&gt;A hydration mismatch occurs when the initial client‑side render doesn’t produce the exact same HTML that the server streamed. React then can’t attach its event handlers cleanly and throws an error. The official &lt;a href="https://nextjs.org/docs/messages/react-hydration-error" rel="noopener noreferrer"&gt;Next.js docs&lt;/a&gt; explain it as "Text content does not match server‑rendered HTML," but the mismatch doesn’t have to be visible text – any attribute, key, or even an invisible &lt;code&gt;id&lt;/code&gt; that differs will trigger it.&lt;/p&gt;

&lt;p&gt;The classic suspects are easy to spot: rendering &lt;code&gt;&amp;lt;time&amp;gt;{new Date().toISOString()}&amp;lt;/time&amp;gt;&lt;/code&gt; or conditionally reading &lt;code&gt;window.innerWidth&lt;/code&gt; during render. What’s far more insidious is the &lt;strong&gt;SVG gradient trap&lt;/strong&gt; — a mismatch caused by a &lt;code&gt;Math.random()&lt;/code&gt; call that lives entirely inside an attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SVG Gradient ID Trap: How Math.random() Broke Our Wordmark
&lt;/h2&gt;

&lt;p&gt;We hit this in our &lt;a href="https://techpotions.com/services/web" rel="noopener noreferrer"&gt;web development studio’s&lt;/a&gt; Next.js project, inside a shared SVG component used for brand wordmarks and decorative blobs.&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="c1"&gt;// ❌ Buggy: Math.random() produces a different id on server and client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gradientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`gradient-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="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;svg&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;defs&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;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;gradientId&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;stop&lt;/span&gt; &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0%"&lt;/span&gt; &lt;span class="na"&gt;stopColor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"var(--color-brand)"&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;stop&lt;/span&gt; &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;stopColor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"var(--color-accent)"&lt;/span&gt; &lt;span class="na"&gt;stopOpacity&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&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;linearGradient&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;defs&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;text&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`url(#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gradientId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Techpotions&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;text&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;svg&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;The component looked harmless — no browser API, no locale, no date. And yet, every server render produced a different &lt;code&gt;id&lt;/code&gt; than the client’s subsequent render. React pointed at the &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; nesting, but the real mismatch was the &lt;code&gt;id&lt;/code&gt; attribute itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Line Fix: useId() Makes Ids Stable Across Server and Client
&lt;/h2&gt;

&lt;p&gt;React ships &lt;code&gt;useId()&lt;/code&gt; precisely for this scenario. It generates an identifier that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable&lt;/strong&gt; across server and client for the same component instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique&lt;/strong&gt; across instances, preventing duplicate ids in the DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replacing &lt;code&gt;Math.random()&lt;/code&gt; with &lt;code&gt;useId()&lt;/code&gt; eliminated the hydration mismatch entirely.&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="c1"&gt;// ✅ Fixed: useId() from React is stable across server and client&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;useId&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;react&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;gradientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&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;svg&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;defs&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;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;gradientId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... stops ... */&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;linearGradient&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;defs&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;text&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`url(#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gradientId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Techpotions&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;text&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;svg&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;No &lt;code&gt;suppressHydrationWarning&lt;/code&gt;, no &lt;code&gt;useEffect&lt;/code&gt; gymnastics. Just one hook. We’ve since made this a standing rule in our project instructions: &lt;strong&gt;no Math.random() in an SVG linearGradient id, use &lt;code&gt;useId()&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The General Rule: Any Impure Render Value Is a Hydration Time Bomb
&lt;/h2&gt;

&lt;p&gt;This bug teaches a larger lesson worth internalising: &lt;strong&gt;a hydration mismatch is any render-time value that is not a pure function of props and state.&lt;/strong&gt; Critically, these values can hide in attributes you wouldn’t think to audit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Impure Source&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Why It Triggers a Mismatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Math.random()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;id={&lt;/code&gt;item-${Math.random()}&lt;code&gt;}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Server and client get different random numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Date.now()&lt;/code&gt; / &lt;code&gt;new Date()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;time&amp;gt;{Date.now()}&amp;lt;/time&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Timestamp differs between renders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div&amp;gt;{localStorage.getItem('theme')}&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only available on the client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window.innerWidth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Layout width={window.innerWidth}&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;window&lt;/code&gt; is undefined on the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navigator.language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Document lang={navigator.language}&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server has no navigator API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request‑time data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetch('/api/user', req)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User‑specific data without proper serialisation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As the community on Stack Overflow affirms, even &lt;a href="https://stackoverflow.com/questions/76874191/why-am-i-getting-hydration-error-for-svg-inside-link-in-next-js" rel="noopener noreferrer"&gt;random ids inside SVGs&lt;/a&gt; can cause the “initial UI does not match” error. The error message pointing at surrounding markup is often a red herring — the real source lives in an attribute value you’d almost never inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Strategy: Grep for Math.random, Date, and Window Before Bisecting
&lt;/h2&gt;

&lt;p&gt;When the error names a component that looks pure, &lt;strong&gt;don’t start bisecting your component tree yet.&lt;/strong&gt; Instead, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'Math.random'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; src/components/marks/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'Date('&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; src/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'window.'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, the offending call was a single word inside a template literal in an &lt;code&gt;id&lt;/code&gt; attribute — easy to miss in manual review, but instantly caught by a simple grep. If you’re in a monorepo or using complex third‑party libraries, this search can save hours of frustration.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Prevent Hydration Mismatches at techpotions
&lt;/h2&gt;

&lt;p&gt;After shipping the &lt;code&gt;useId()&lt;/code&gt; fix, we added a lint rule that flags any &lt;code&gt;Math.random()&lt;/code&gt; inside JSX‑returning functions. It’s one of several defensive practices we bake into our Next.js projects. For example, when we built a smooth‑scroll animation that needed to work on Windows, we &lt;a href="https://techpotions.com/lab/lenis-windows-smooth-scroll-fix" rel="noopener noreferrer"&gt;fixed a Lenis scroll issue&lt;/a&gt; by ensuring the client‑only logic never leaked into the server render.&lt;/p&gt;

&lt;p&gt;If your app is plagued by hydration errors you can’t trace, our web development team offers &lt;a href="https://techpotions.com/services/web" rel="noopener noreferrer"&gt;code‑reviews and focused debugging sessions&lt;/a&gt;. We focus on the attribute‑level detail that generic tooling often misses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;Ready to start?&lt;/a&gt; Let’s make your Next.js app bulletproof.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does Math.random() cause a hydration mismatch?
&lt;/h3&gt;

&lt;p&gt;Because server and client rendering are independent processes. &lt;code&gt;Math.random()&lt;/code&gt; returns a different number each time, so the generated gradient id is different on the server than on the client. React then detects that the initial HTML doesn’t match what the client expects and throws the mismatch error.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does useId() solve the problem?
&lt;/h3&gt;

&lt;p&gt;React’s &lt;code&gt;useId()&lt;/code&gt; generates a stable, deterministic identifier that is guaranteed to be identical on the server and client for the same component instance. It doesn’t rely on impure data, so the id matches every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can other attributes aside from id trigger a hydration mismatch?
&lt;/h3&gt;

&lt;p&gt;Absolutely. Any attribute — class names, keys, &lt;code&gt;data-*&lt;/code&gt; attributes, inline styles, or even render‑time logic that affects markup structure — can trigger a mismatch if it derives from impure sources like &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, or request‑time data. Treat every attribute as a potential leak point.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>hydration</category>
      <category>svg</category>
    </item>
    <item>
      <title>Getting Reliable JSON Out of an LLM in Production</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:48:33 +0000</pubDate>
      <link>https://dev.to/techpotions/getting-reliable-json-out-of-an-llm-in-production-170i</link>
      <guid>https://dev.to/techpotions/getting-reliable-json-out-of-an-llm-in-production-170i</guid>
      <description>&lt;p&gt;LLM structured output JSON schema is the foundation of any production pipeline that relies on machine‑readable responses. But even with a strict schema and a well‑tuned prompt, our daily content‑generation pipeline broke twice—once because a provider silently dropped support, and once because a widely‑recommended parsing trick ate our own data. Neither failure appeared in local testing. Both teach the same lesson: a structured output contract is not a reliability guarantee; you must still code for the real world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLM JSON Parsing Still Breaks in Production
&lt;/h2&gt;

&lt;p&gt;The promise of llm structured output json schema is that you get a pristine JSON object that matches your definition. In practice, two things go wrong. First, not every model gateway supports &lt;code&gt;json_schema&lt;/code&gt; with &lt;code&gt;strict: true&lt;/code&gt;; some return HTTP 400. Second, many parsers blindly strip markdown code fences, a heuristic that corrupts any field that legitimately contains triple‑backtick code blocks—like the Markdown body of a generated article. We hit both in a pipeline that runs daily behind our &lt;a href="https://techpotions.com/services/ai" rel="noopener noreferrer"&gt;AI‑powered comparison pages&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Right Way to Request Structured Output
&lt;/h2&gt;

&lt;p&gt;Start by setting &lt;code&gt;response_format&lt;/code&gt; to &lt;code&gt;{ type: "json_schema", json_schema: { strict: true, schema: yourSchema } }&lt;/code&gt;. This eliminates prose drift and guarantees valid JSON shape. But assume nothing: read the model name at call time so a runtime environment override can swap models without a redeploy. If you hardcode the model at module load, your only escape during an outage is a full deploy.&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;// Always resolve the model at call time, not at module load&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;getStructuredCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&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;STRUCTURED_MODEL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- runtime override friendly&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json_schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;json_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;schema&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="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failure Mode #1: Provider Capability Drift
&lt;/h2&gt;

&lt;p&gt;Even when your API client sends a valid &lt;code&gt;json_schema&lt;/code&gt; request, an upstream provider may reject it. Some gateways don’t support &lt;code&gt;strict: true&lt;/code&gt; at all and return HTTP 400. Our client catches that exact status and retries with &lt;code&gt;response_format: { type: "json_object" }&lt;/code&gt;. The model then outputs unconstrained JSON (possibly wrapped in prose), so we follow up with a tolerant parser. The key insight: &lt;code&gt;strict json_schema&lt;/code&gt; is the right default, not a lowest‑common‑denominator compromise. Detect and degrade, never pre‑weaken your request.&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="k"&gt;try&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&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="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json_schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;json_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&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="nx"&gt;signal&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Provider rejected strict schema; fall back to json_object&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;400 on strict json_schema; retrying with json_object&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;fallbackRes&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&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="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json_object&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="nx"&gt;signal&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleJsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallbackRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleJsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failure Mode #2: When Fence‑Stripping Parsers Mangle Content
&lt;/h2&gt;

&lt;p&gt;The usual advice for parsing LLM JSON is to strip triple‑backtick fences before calling &lt;code&gt;JSON.parse&lt;/code&gt;. That heuristic is &lt;strong&gt;actively dangerous&lt;/strong&gt; when any field in your schema contains fenced code blocks. Our &lt;a href="https://techpotions.com/lab/stop-ai-blog-pipeline-hallucinations" rel="noopener noreferrer"&gt;blog generation pipeline&lt;/a&gt; produces articles whose &lt;code&gt;markdown&lt;/code&gt; field routinely includes code blocks. A fence‑stripper that greedily removes everything between backticks will corrupt the payload. We learned this the hard way when a generated article lost half its code examples.&lt;/p&gt;

&lt;p&gt;Our parser does the opposite: it tries &lt;code&gt;JSON.parse(raw)&lt;/code&gt; &lt;em&gt;first&lt;/em&gt;, because native structured output should never produce fences. Only if that initial parse throws do we extract the substring from the first &lt;code&gt;{&lt;/code&gt; to the last &lt;code&gt;}&lt;/code&gt;. We never remove backticks.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tolerantParseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="nb"&gt;Record&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;// Try direct parse first (works for native structured output)&lt;/span&gt;
  &lt;span class="k"&gt;try&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fall through */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Fallback: grab everything between the first { and the last }&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstBrace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;lastBrace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;}&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;firstBrace&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lastBrace&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lastBrace&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;firstBrace&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;extracted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstBrace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastBrace&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No JSON object found&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach preserves embedded code fences, handles models that occasionally wrap JSON in a single pair of backticks (the braces still exist), and doesn’t penalize providers that return pristine JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Patterns That Keep Pipelines Running
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep the system prompt explicit about forbidden invention.&lt;/strong&gt; Structured output guarantees shape, not truth. Our prompt includes: “If a requested fact is not supported by the provided sources, leave it out rather than inventing it.” Schema compliance and factual accuracy are unrelated problems; &lt;a href="https://techpotions.com/lab/how-to-add-evals-to-an-llm-feature" rel="noopener noreferrer"&gt;mixing them leads to beautifully‑typed hallucinations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a lightweight evaluation step&lt;/strong&gt; for mission‑critical pipelines. Even after parsing succeeds, run a few assertions against the output—expected keys, value types, content‑based rules. This catches the infamous “valid JSON, wrong content” failure without a heavy ML eval framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log every raw response&lt;/strong&gt; (sanitized if needed) for at least a few days. When parsing fails in production, the first question is always “what did the model actually return?” Having the raw payload in your logs turns a mystery into a five‑minute fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my LLM return 400 when I request json_schema with strict mode?
&lt;/h3&gt;

&lt;p&gt;Some providers or gateways don’t support &lt;code&gt;strict: true&lt;/code&gt;. The fix is to catch the 400 status and retry the same messages with &lt;code&gt;response_format: { type: "json_object" }&lt;/code&gt;, then use a tolerant parser that handles unconstrained output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can’t I just strip triple backticks before parsing JSON from an LLM?
&lt;/h3&gt;

&lt;p&gt;Avoid blanket fence‑stripping. If any field in your schema can contain backtick code blocks (like Markdown), the stripping logic will corrupt the payload. Instead, attempt &lt;code&gt;JSON.parse&lt;/code&gt; directly, then fall back to extracting text between the first &lt;code&gt;{&lt;/code&gt; and last &lt;code&gt;}&lt;/code&gt; if that fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does structured output prevent hallucinations?
&lt;/h3&gt;

&lt;p&gt;No. Structured output guarantees that the JSON matches your schema, but not that the content is correct. Always prompt the model to omit unsupported facts rather than invent them, and consider adding a post‑parse validation or eval step for high‑stakes pipelines.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>structuredoutput</category>
      <category>jsonschema</category>
      <category>production</category>
    </item>
    <item>
      <title>Block AI Crawlers: The 15 Bots That Matter</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:34:25 +0000</pubDate>
      <link>https://dev.to/techpotions/block-ai-crawlers-the-15-bots-that-matter-3nb4</link>
      <guid>https://dev.to/techpotions/block-ai-crawlers-the-15-bots-that-matter-3nb4</guid>
      <description>&lt;p&gt;Most lists that claim to help you &lt;strong&gt;block AI crawlers&lt;/strong&gt; are copy-pasted and dangerously wrong about the two tokens that actually matter. Sorting them properly is not an abstract taxonomy exercise. It is the single decision that determines whether your content vanishes from AI answers while training continues — or vice versa.&lt;/p&gt;

&lt;p&gt;We maintain the crawler registry in &lt;code&gt;lib/ai-crawlers.ts&lt;/code&gt; that powers &lt;a href="https://techpotions.com/tools/ai-robots-txt-generator" rel="noopener noreferrer"&gt;techpotions’ free AI robots.txt generator&lt;/a&gt;. Every agent string and description was verified against the operator’s own crawler documentation. The registry holds 15 verified bots across four categories, and that four-way split is this article’s structure, because the categories map directly to what blocking costs you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two tokens almost everyone gets wrong
&lt;/h2&gt;

&lt;p&gt;Before the list, the single most important correction to make, and almost every listicle on this query gets it wrong: &lt;strong&gt;Google-Extended&lt;/strong&gt; and &lt;strong&gt;Applebot-Extended are not crawlers.&lt;/strong&gt; They are robots.txt tokens — product controls that govern whether your content is used for Gemini and Apple foundation-model training.&lt;/p&gt;

&lt;p&gt;Blocking &lt;code&gt;Google-Extended&lt;/code&gt; does not affect Google Search crawling, Google ranking, or regular Applebot search indexing. People block them believing they are opting out of AI Overviews, and are actually opting out of nothing they think they are, while leaving search indexing completely untouched.&lt;/p&gt;

&lt;p&gt;Platforms have started wiring these tokens into one-click controls. Cloudflare’s managed robots feature, released mid-2025, lets you add AI crawler rules through a dashboard toggle rather than editing a raw file — but the underlying token logic above still applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Block AI crawlers: the four categories that decide the cost
&lt;/h2&gt;

&lt;p&gt;Every AI crawler we track belongs to one of four categories. The category tells you the cost of blocking it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Training crawlers&lt;/strong&gt; scrape pages to feed a model that may never cite you. Blocking them is a defensive data decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assistant crawlers&lt;/strong&gt; fetch pages to answer a live user’s question and can cite and link you. Blocking them removes you from AI answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search crawlers&lt;/strong&gt; underpin traditional or AI search indexes. Blocking them is a visibility trade-off with ranking consequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrapers&lt;/strong&gt; operate with little transparency and rarely honor robots.txt. A block is a declaration of intent, not a guarantee of enforcement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blocking everything is the lazy answer, and it costs AI-answer visibility. The entire point of this list is to let you decide bot by bot, with your eyes open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training crawlers: default-deny if you opt out of training
&lt;/h2&gt;

&lt;p&gt;These bots feed foundation models. They will not send you traffic or citations. The trade-off is simple: if you want to &lt;strong&gt;block AI crawlers&lt;/strong&gt; from training on your work, start here.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What you give up by blocking it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPTBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Crawls content that may be used to train OpenAI’s generative foundation models, including the models powering ChatGPT.&lt;/td&gt;
&lt;td&gt;The clearest training opt-out. Blocking this has zero impact on whether ChatGPT can answer a live user query about your page — that is a separate assistant crawler.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CCBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Common Crawl&lt;/td&gt;
&lt;td&gt;Crawls the web to build the Common Crawl dataset, a massive open repository of web pages used as training data by many LLM builders.&lt;/td&gt;
&lt;td&gt;You opt out of a foundational training dataset used across the industry, not just a single model. It sends no user traffic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ClaudeBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Crawls content that may be used to train Claude. Separate from Anthropic’s assistant crawler.&lt;/td&gt;
&lt;td&gt;You lose no AI-answer visibility. Claude’s live-fetch assistant crawler is a completely different agent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google-Extended&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;A token, not a bot. Controls whether content crawled by Google’s regular search crawler can be used for Gemini foundation-model training.&lt;/td&gt;
&lt;td&gt;Opting out does not affect ranking, Google Search crawling, indexing, or inclusion in AI Overviews. You are only blocking Gemini training.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Applebot-Extended&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;A token, not a bot. Controls whether content crawled by regular Applebot can be used for Apple foundation-model training.&lt;/td&gt;
&lt;td&gt;Does not affect Apple search indexing. Blocking this does nothing to Siri or Apple Intelligence live answers.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Assistant crawlers: block these and you leave the conversation
&lt;/h2&gt;

&lt;p&gt;Assistant crawlers fetch your page to answer a real user’s question. They can cite and link you. This is the category where blanket blocking most often hurts traffic, because the site owner thought they were stopping training.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What you give up by blocking it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT-User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Fetches pages on behalf of ChatGPT users who ask questions that require live web retrieval. It cites sources.&lt;/td&gt;
&lt;td&gt;You remove your content from ChatGPT’s answers entirely. No citation, no referral traffic, no visibility in the assistant that millions use for real-time research.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OAI-SearchBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Crawls for OpenAI’s web search product. Used for surface-level search indexing rather than training.&lt;/td&gt;
&lt;td&gt;You disappear from OpenAI’s native search experience, distinct from ChatGPT’s browsing. An SEO visibility call, not a training call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PerplexityBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Fetches pages to generate Perplexity’s AI answers with inline citations.&lt;/td&gt;
&lt;td&gt;Blocking it means Perplexity cannot pull your page into answers, removing a growing source of cited traffic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cohere-AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;Fetches web content to power Cohere’s Retrieval-Augmented Generation pipeline, with citations.&lt;/td&gt;
&lt;td&gt;You drop out of an enterprise RAG stack. If your content answers high-stakes or business queries, this is a concrete loss.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Search crawlers: the visibility trade-off
&lt;/h2&gt;

&lt;p&gt;These bots feed search indexes, including AI search. Blocking them carries ranking consequences — not theoretical ones.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What you give up by blocking it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Googlebot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Decades-old search crawler powering Google Search, Discover, and now the AI Overviews index.&lt;/td&gt;
&lt;td&gt;You leave the Google search ecosystem entirely. There is no separate token to opt out of AI Overviews while keeping search ranking. Googlebot feeds both.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bingbot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Crawls for Bing Search and powers the retrieval layer behind Microsoft Copilot answers.&lt;/td&gt;
&lt;td&gt;You vanish from Bing Search and from Copilot citations. One block, two surfaces.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DuckDuckBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DuckDuckGo&lt;/td&gt;
&lt;td&gt;Crawls for DuckDuckGo’s web index, which also underpins DDG’s AI answer features.&lt;/td&gt;
&lt;td&gt;You leave a search engine that, while smaller, fuels AI answers that do cite. Visibility trade-off.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Scrapers: a block is the right signal, but enforcement is uneven
&lt;/h2&gt;

&lt;p&gt;These crawlers aggregate content, often for commercial datasets or internal models. The compliance story is thinner, and four entries in our registry carry explicit caveats. &lt;code&gt;Bytespider&lt;/code&gt; is the clearest case: it has no official crawler documentation at all, so our entry states exactly that rather than pretending a policy exists. A block here signals intent. It is not a technical guarantee.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What you give up by blocking it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bytespider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ByteDance&lt;/td&gt;
&lt;td&gt;Widely observed to crawl for TikTok and Douyin recommendations, potentially feeding internal research datasets. No official docs exist.&lt;/td&gt;
&lt;td&gt;There is no documented user-facing benefit to keeping it. The cost is speculative scraping with zero transparency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FacebookBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;td&gt;Crawls for article previews, ad quality, and internal ML research. The boundary is deliberately blurred.&lt;/td&gt;
&lt;td&gt;You lose rich Facebook share cards and article previews. The same agent may also feed training, and Meta does not separate them with distinct tokens.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazonbot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;Crawls for Alexa, product search, and AWS AI services. Amazon’s documentation states metadata enrichment as a secondary purpose.&lt;/td&gt;
&lt;td&gt;Blocking it can affect how your products appear in Alexa answers and the Amazon shopping index, but the crawl does not offer referral traffic back to your site.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to block AI crawlers without breaking visibility
&lt;/h2&gt;

&lt;p&gt;The right approach is a per-bot robots.txt rule set that maps to your actual goals. A site that wants training opt-out but AI-answer visibility might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: GPTBot
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: ChatGPT-User
Allow: /

User-agent: PerplexityBot
Allow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This blocks foundation-model training while keeping your content in ChatGPT and Perplexity answers. We built the &lt;a href="https://techpotions.com/tools/ai-robots-txt-generator" rel="noopener noreferrer"&gt;AI robots.txt generator&lt;/a&gt; to produce these split-category files in seconds, because the mistake we kept seeing was people pasting a blockade that also removed them from AI answers. For the specification behind agent instructions beyond robots.txt, see &lt;a href="https://techpotions.com/lab/llms-txt-examples" rel="noopener noreferrer"&gt;llms.txt examples&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are tuning this as part of a broader AI visibility play, our &lt;a href="https://techpotions.com/services/growth-labs" rel="noopener noreferrer"&gt;Growth Labs&lt;/a&gt; team works on the question from the traffic side: what you gain by being in the models people are actually asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does blocking Google-Extended stop my content appearing in AI Overviews?
&lt;/h3&gt;

&lt;p&gt;No. Blocking &lt;code&gt;Google-Extended&lt;/code&gt; tells Google not to use crawled content for Gemini foundation-model training. It does not affect Google Search crawling, ranking, or inclusion in AI Overviews. Blocking it does not opt you out of AI Overviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can blocking AI crawlers hurt my visibility in AI-powered answers?
&lt;/h3&gt;

&lt;p&gt;Yes. Assistant crawlers like &lt;code&gt;ChatGPT-User&lt;/code&gt; fetch pages to answer a live user’s question and can cite and link you. If you block the assistant category, those tools cannot retrieve your content at all, and you become invisible in their answers. The typical mistake is a blanket block that removes sites from AI answers while the owner intended only to opt out of training.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can’t I just use a NoAI meta tag to stop training?
&lt;/h3&gt;

&lt;p&gt;Because it is not a reliable mechanism against the scrapers that ignore it. &lt;code&gt;Bytespider&lt;/code&gt; has no official crawler documentation, so there is no verifiable opt-out procedure. A &lt;code&gt;&amp;lt;meta name="robots" content="noai"&amp;gt;&lt;/code&gt; tag is not honored by all training crawlers and offers no protection against bad-faith scraping. A robots.txt rule is the stronger, more widely recognized signal.&lt;/p&gt;

&lt;p&gt;Ready to set the exact rules that match your risk tolerance and visibility goals? &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;Start with our crawler registry tool.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aicrawlers</category>
      <category>robotstxt</category>
      <category>seo</category>
      <category>aivisibility</category>
    </item>
    <item>
      <title>Next.js Sitemap Not Updating? Here's the Real Fix</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:34:14 +0000</pubDate>
      <link>https://dev.to/techpotions/nextjs-sitemap-not-updating-heres-the-real-fix-2257</link>
      <guid>https://dev.to/techpotions/nextjs-sitemap-not-updating-heres-the-real-fix-2257</guid>
      <description>&lt;p&gt;Next.js Sitemap Not Updating? Here's the Real Fix&lt;/p&gt;

&lt;p&gt;If your Next.js sitemap is not updating after you publish new content, you're dealing with a cache-coherence bug that almost nobody writes up. It has an exact symptom, a reproducible root cause, and a one-line fix. This is the guide you'll wish you had the moment you notice &lt;code&gt;/sitemap.xml&lt;/code&gt; serving fewer entries than your real site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom: your sitemap lags behind your published content
&lt;/h2&gt;

&lt;p&gt;The mismatch is impossible to miss once you look. On our own site, &lt;code&gt;/lab&lt;/code&gt; lists 11 published posts, yet &lt;code&gt;/sitemap.xml&lt;/code&gt; shows only 7. Same database, same deploy, two different answers. If that gap sounds familiar, you're in the right place.&lt;/p&gt;

&lt;p&gt;You might have checked your &lt;code&gt;afterChange&lt;/code&gt; hook, verified that &lt;code&gt;revalidateTag('posts')&lt;/code&gt; fires, and even confirmed that the tagged data refreshes — only to find the sitemap still frozen. That's because the problem lives between two cache layers, not inside the data fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why revalidateTag doesn't fix a stale Next.js sitemap
&lt;/h2&gt;

&lt;p&gt;The answer lies in what &lt;code&gt;sitemap.ts&lt;/code&gt; actually is. According to the &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap" rel="noopener noreferrer"&gt;Next.js Metadata Files: sitemap.xml&lt;/a&gt; documentation, it's a special Route Handler. And like any Route Handler, Next.js caches its rendered output by default.&lt;/p&gt;

&lt;p&gt;Here's what happened in our own repository (this bug is documented in a comment at the top of &lt;code&gt;app/(frontend)/sitemap.ts&lt;/code&gt; because it cost real indexation time):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We read content from Payload using &lt;code&gt;unstable_cache&lt;/code&gt;, tagged with the collection slug &lt;code&gt;posts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;afterChange&lt;/code&gt; hook called &lt;code&gt;revalidateTag('posts')&lt;/code&gt; whenever a post was published.&lt;/li&gt;
&lt;li&gt;That call did work — it invalidated the inner &lt;code&gt;unstable_cache&lt;/code&gt; data entry.&lt;/li&gt;
&lt;li&gt;But the route's statically-rendered &lt;strong&gt;outer&lt;/strong&gt; XML output was never re-run. The frozen route output kept serving the old XML built from the old data, long after the inner cache was refreshed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two cache layers. Tag-based revalidation busted the inner one, but the outer route handler cache was never told to re-execute. That's the missing piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix: route-level ISR on the sitemap
&lt;/h2&gt;

&lt;p&gt;Tell the sitemap route to re-render on a bounded interval:&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/(frontend)/sitemap.ts&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="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seconds — re-render at most once per hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. With this line, the whole route re-executes after 3600 seconds and always picks up newly published documents. The sitemap is crawler-only traffic, so an hour of staleness is irrelevant. Even better, the bounded interval deduplicates the Payload read to at most once per hour, so you're not hammering the database on every bot request.&lt;/p&gt;

&lt;p&gt;Once route-level ISR is in place, the inner &lt;code&gt;unstable_cache&lt;/code&gt; becomes redundant. The route itself is now invalidating on a schedule, so you can drop the wrapper and simplify your data fetching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drop the inner cache after adding revalidate
&lt;/h2&gt;

&lt;p&gt;Before, your sitemap might have looked like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_cache&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;next/cache&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;getPayloadPosts&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/payload&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;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;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;unstable_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;getPayloadPosts&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;sitemap-posts&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="na"&gt;tags&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;posts&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;span class="c1"&gt;// generate XML…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After setting &lt;code&gt;export const revalidate = 3600&lt;/code&gt;, you can remove the &lt;code&gt;unstable_cache&lt;/code&gt; wrapper entirely:&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="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="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;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;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;getPayloadPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// generate XML…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route-level ISR already ensures the handler re-runs periodically, and on-demand revalidation can still be triggered with &lt;code&gt;revalidatePath('/sitemap.xml')&lt;/code&gt; from your publish hook if you need an instant refresh. No more stale XML.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson: tag-based revalidation invalidates data, not rendered output
&lt;/h2&gt;

&lt;p&gt;This bug taught us a rule that applies to every part of Next.js where you stack a data cache inside a route cache:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;revalidateTag clears your &lt;em&gt;data&lt;/em&gt;, not the route that renders it.&lt;/strong&gt; If the route itself is cached (static generation, full route cache, etc.), invalidating inner data leaves the outer rendered output untouched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Whenever you have both layers, you must either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set route-level &lt;code&gt;revalidate&lt;/code&gt; to give the outer layer a time bound, or&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;revalidatePath&lt;/code&gt; on the route itself to manually bust the rendered output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the piece that the &lt;code&gt;revalidateTag&lt;/code&gt; documentation doesn't spell out, and it's the difference between a sitemap that stays fresh and one that silently sabotages your indexation.&lt;/p&gt;

&lt;p&gt;After fixing your sitemap, tell search engines about the new content immediately — our &lt;a href="https://techpotions.com/lab/indexnow-next-js" rel="noopener noreferrer"&gt;IndexNow integration for Next.js&lt;/a&gt; pushes updates the moment you publish.&lt;/p&gt;

&lt;p&gt;Struggling with caching or Next.js performance in general? Our team builds products where this stuff is ironed out before it bites you — &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;start a project&lt;/a&gt; and we'll make sure your site's infrastructure stays boringly reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is my Next.js sitemap not updating even though revalidateTag works?
&lt;/h3&gt;

&lt;p&gt;revalidateTag only clears data caches tagged by unstable_cache. Your sitemap route's rendered output is cached separately as a static route handler. To update the sitemap, you need to either revalidate the path with revalidatePath('/sitemap.xml') or set route-level ISR with export const revalidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use unstable_cache in my sitemap route after adding revalidate?
&lt;/h3&gt;

&lt;p&gt;No. After setting revalidate on the route, Next.js will re-render the whole handler periodically, making the inner data cache redundant. Remove unstable_cache to simplify.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the recommended revalidate interval for a sitemap?
&lt;/h3&gt;

&lt;p&gt;An hour (3600 seconds) is safe. Sitemaps are consumed by crawlers sporadically, so a small delay won't hurt indexation, while it protects your database from unnecessary reads.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sitemap</category>
      <category>caching</category>
      <category>isr</category>
    </item>
    <item>
      <title>What AI-Assisted Posts Actually Do for a New Domain</title>
      <dc:creator>techpotions</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:28:50 +0000</pubDate>
      <link>https://dev.to/techpotions/what-ai-assisted-posts-actually-do-for-a-new-domain-77p</link>
      <guid>https://dev.to/techpotions/what-ai-assisted-posts-actually-do-for-a-new-domain-77p</guid>
      <description>&lt;p&gt;Do AI generated blog posts rank? We ran our own AI-assisted publishing pipeline on a brand-new domain (techpotions.com) for six months, and the honest answer is: not yet, and not for the reasons you think. The raw numbers are unflattering, but the diagnosis—and the rebuild that followed—is what matters for any founder being sold an AI content engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our zero‑traffic AI content pipeline actually looked like
&lt;/h2&gt;

&lt;p&gt;Our baseline, pulled in July 2026, was brutal. Semrush showed zero organic traffic, an Authority Score of 2, and exactly one genuine referring domain (a nofollowed Medium mention). Google Search Console revealed a tiny trickle over 28 days: 16 clicks from 341 impressions at an average position of 12.1. We had published roughly 40 posts to our &lt;code&gt;/lab&lt;/code&gt; section, but the numbers told a different story behind the scenes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semrush Authority Score&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organic traffic (Semrush)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organic keywords (Semrush)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referring domains (genuine)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages indexed (GSC)&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages not indexed (GSC)&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clicks (28 days, GSC)&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impressions (28 days, GSC)&lt;/td&gt;
&lt;td&gt;341&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg. position (GSC)&lt;/td&gt;
&lt;td&gt;12.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those 56 unindexed URLs weren’t a sign of low-quality content being rejected. They were a sign of a broken technical foundation. 33 of them were &lt;code&gt;www&lt;/code&gt; duplicates of pages that already existed at the apex domain—so they were never separate content at all. Another 2 were stale redirects left over from a hosting migration. The rest were thin or orphaned pages that the generator had produced in volume mode.&lt;/p&gt;

&lt;p&gt;Volume was never the constraint. The pipeline was a volume machine, recycling trending news into posts that summarized what was already ranked elsewhere. It was writing, but it wasn’t saying anything that hadn’t been said a hundred times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do AI generated blog posts rank? Here’s why ours didn’t.
&lt;/h2&gt;

&lt;p&gt;Three things were broken, and none of them were fixed by writing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Indexation sabotage.&lt;/strong&gt; Before you measure traffic, measure indexation. Of our 56 unindexed URLs, 35 were completely avoidable—33 &lt;code&gt;www&lt;/code&gt; duplicates and 2 stale redirects. Google had crawled the right pages but chose to ignore the wrong ones, and we had given it twice as many wrong ones as right ones. When you fix that, you don’t get better content; you just stop actively sabotaging your own presence. We cleaned up canonical tags, set up proper redirects, and used &lt;a href="https://techpotions.com/lab/indexnow-next-js" rel="noopener noreferrer"&gt;IndexNow for Next.js&lt;/a&gt; to tell search engines about the canonical versions immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Domain authority gating.&lt;/strong&gt; Authority Score 2 with one real referring domain does not rank, regardless of how good any individual post is. No amount of publishing fixes a referring-domain problem. This is the gap that most AI content engines conveniently ignore. &lt;a href="https://seranking.com/blog/ai-content-experiment/" rel="noopener noreferrer"&gt;SE Ranking’s own experiment&lt;/a&gt; with AI-generated articles on an established blog saw traffic gains precisely because the domain already had authority. Without that, even well-written, helpful AI-assisted posts remain invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Undifferentiated output.&lt;/strong&gt; Our earlier pipeline recycled trending news. It produced posts that merely summarized what was already ranking. That’s the fastest way to hit the “thin content” wall, even if Google’s &lt;a href="https://developers.google.com/search/blog/2023/02/google-search-and-ai-content" rel="noopener noreferrer"&gt;guidance on AI content&lt;/a&gt; says it’s not automatically spam. The posts weren’t deceptive; they were just redundant. As &lt;a href="https://www.reddit.com/r/SEO/comments/13jty2r/has_anyone_tried_using_aigenerated_content_on/" rel="noopener noreferrer"&gt;one Reddit thread&lt;/a&gt; points out, the best AI-assisted workflows use research and first-hand data, not just summarization. We had to kill the news-recycling mode entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline rebuild that stopped writing when there was nothing to say
&lt;/h2&gt;

&lt;p&gt;We rebuilt the generator so that it will only draft a topic that carries a real proof point and a first-hand expert note. If the backlog has neither, the generator produces zero posts by design. An empty backlog now means zero output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudo-code for the rule we baked into the pipeline
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proof_point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expert_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# No output until a real expert note exists
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;draft_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the difference between “using AI to write” and “using AI to scale what you already know.” &lt;a href="https://www.averi.ai/how-to/is-ai-generated-content-good-for-seo-balancing-automation-with-search-best-practices" rel="noopener noreferrer"&gt;Averi’s breakdown&lt;/a&gt; gets it right: AI-generated content isn’t good or bad for SEO; your process for creating AI-assisted content determines whether it helps or hurts. Our process now requires a human to bring something original before the model gets to type a single word. We also built in safeguards against the hallucination problem that plagues AI pipelines—&lt;a href="https://techpotions.com/lab/stop-ai-blog-pipeline-hallucinations" rel="noopener noreferrer"&gt;stopping hallucinations before they publish&lt;/a&gt; is a non-negotiable layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transferable rules for founders buying an AI content engine
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Measure indexation before you measure traffic.&lt;/strong&gt; Unindexed posts are not slow—they are absent. If half your pages aren’t in Google’s index, no amount of content tweaking will help. Fix duplicate-host and redirect issues before blaming the copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judge month one on indexation and keyword coverage, not sessions.&lt;/strong&gt; A new domain with zero authority will not earn meaningful traffic in the first month, no matter how good the content is. Track how many pages are indexed and how many queries they show up for in Search Console—that’s your real leading indicator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI removes the drafting cost, not the authority cost.&lt;/strong&gt; &lt;a href="https://thecreativestable.com/2026/01/15/i-tested-ai-for-automated-blogging-heres-what-happened/" rel="noopener noreferrer"&gt;The CreativeStable experiment&lt;/a&gt; with automated AI blogging hit the same wall: volume alone doesn’t build trust. You still need to earn links the old-fashioned way. AI-assisted writing changes your publishing rate and nothing about your link profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Require first-hand proof points or the pipeline stays silent.&lt;/strong&gt; If your topic backlog is just a list of keywords, you’ll produce the same summary everyone else has. The only AI content that earns a place on the web is built on something the model didn’t read on the internet—your data, your experiment, your expert note.&lt;/p&gt;

&lt;p&gt;These rules are exactly what we’ve baked into &lt;a href="https://techpotions.com/services/growth-labs" rel="noopener noreferrer"&gt;Growth Labs&lt;/a&gt;, where we pair AI-assisted drafting with real authority-building tactics so founders don’t have to learn the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start building an AI-assisted content engine that actually works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clean up your technical foundation.&lt;/strong&gt; Fix canonicalization, eliminate &lt;code&gt;www&lt;/code&gt; duplicates, and remove stale redirects. Use &lt;a href="https://techpotions.com/lab/indexnow-next-js" rel="noopener noreferrer"&gt;IndexNow&lt;/a&gt; to push canonical URLs to search engines the moment you publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a topic backlog that requires proof points.&lt;/strong&gt; Every topic should have a “why us” column: a data point, a screenshot, a customer story, a build log. If you can’t fill that column, the topic doesn’t get written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the generator to produce zero output when the backlog is empty.&lt;/strong&gt; The pipeline should not have a fallback mode that summarizes trending news. “No expert note” means “no post.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track indexation and keyword coverage, not sessions.&lt;/strong&gt; In month one, your goal is to get 90%+ of your pages indexed and to see impressions climbing for a growing set of queries. Traffic comes later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest in link building separately.&lt;/strong&gt; The AI pipeline is your drafting engine; you still need a link-building strategy. Treat them as two independent workstreams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ready to build an AI-assisted content engine without the expensive mistakes? &lt;a href="https://techpotions.com/start" rel="noopener noreferrer"&gt;Start here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do AI generated blog posts rank on a new domain?
&lt;/h3&gt;

&lt;p&gt;Not automatically. On a brand-new domain with zero authority, even genuinely helpful AI-assisted posts won’t rank if they are not indexed, if the technical foundation is broken, or if the content itself is undifferentiated. Our own pipeline produced zero organic traffic according to Semrush, despite 40 posts, because indexation issues and domain authority gate kept them invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the first thing to fix before using AI-generated content on my site?
&lt;/h3&gt;

&lt;p&gt;Fix indexation first. Before writing a single post, clean up duplicate canonical URLs, stale redirects, and www/non-www conflicts. Then build a topic backlog that requires a first-hand proof point and an expert note, and set your generator to produce zero posts when the backlog is empty. Finally, track keyword coverage and indexed pages, not sessions, in month one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I rank a new domain with AI content alone, without any link building?
&lt;/h3&gt;

&lt;p&gt;No. AI removes the drafting cost, not the authority cost. A domain at Authority Score 2 with one real referring domain will not rank, regardless of post quality. You need a separate link-building strategy alongside AI-assisted publishing to earn the referring domains that signal trust.&lt;/p&gt;

</description>
      <category>aicontent</category>
      <category>seo</category>
      <category>buildinpublic</category>
      <category>contentstrategy</category>
    </item>
  </channel>
</rss>
