<?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: Nayan Kyada</title>
    <description>The latest articles on DEV Community by Nayan Kyada (@nayankyada).</description>
    <link>https://dev.to/nayankyada</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%2F2638501%2Fb0cc4a93-db40-4087-9ff8-b4c2debac8a1.jpg</url>
      <title>DEV Community: Nayan Kyada</title>
      <link>https://dev.to/nayankyada</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nayankyada"/>
    <language>en</language>
    <item>
      <title>Sanity Editors Publishing Changes That Don't Show on the Live Site</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sat, 07 Nov 2026 11:13:29 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-editors-publishing-changes-that-dont-show-on-the-live-site-2608</link>
      <guid>https://dev.to/nayankyada/sanity-editors-publishing-changes-that-dont-show-on-the-live-site-2608</guid>
      <description>&lt;p&gt;Your editors click publish in Sanity, refresh the page, and see the old content. They publish again, then message you. A Next.js and Sanity site that serves stale content after publishing is almost always a revalidation wiring problem, not a Sanity problem, and it is fixable without a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the symptoms usually look like
&lt;/h2&gt;

&lt;p&gt;The pattern tells you a lot about the cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content updates only after a redeploy. That points to fully static pages with no revalidation at all.&lt;/li&gt;
&lt;li&gt;Content updates after an hour or a day. That is a time-based &lt;code&gt;revalidate&lt;/code&gt; value doing the work, and the webhook is missing or failing.&lt;/li&gt;
&lt;li&gt;Some pages update and others do not. That is usually a tag or path mismatch: the webhook revalidates what it knows about, and the stale pages are cached under something else.&lt;/li&gt;
&lt;li&gt;It works in Studio preview but not on the public URL. Draft mode bypasses the cache, so preview looks fine while production is stale.&lt;/li&gt;
&lt;li&gt;It updates for you but not for editors in another country. That suggests a CDN or browser caching layer in front of the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs to ignore it
&lt;/h2&gt;

&lt;p&gt;Stale content is not just annoying. Editors stop trusting the Studio, start asking developers to redeploy for every typo, and your marketing team loses the main reason you went headless. Price changes, legal copy, and campaign pages that show old information carry real risk. Some teams react by setting revalidation to zero, which trades the problem for slow pages and a larger API bill. I covered that second failure in &lt;a href="https://nayankyada.com/blog/sanity-api-bill-spiking-why-requests-explode-on-nextjs-sites" rel="noopener noreferrer"&gt;Sanity API bill spiking? Why requests explode on Next.js sites&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: the webhook never fires or fails silently
&lt;/h2&gt;

&lt;p&gt;Open the Sanity project settings, go to the API section, and look at your webhook and its delivery attempts. Check three things: the filter, the projection, and the response status. A common mistake is a filter limited to one document type, so new types added later never trigger a refresh. Another is the endpoint URL pointing at a preview deployment or an old domain.&lt;/p&gt;

&lt;p&gt;If deliveries show 401 or 403, your secret verification is rejecting valid requests. If they show 404, the route handler path is wrong. The full verified setup is in &lt;a href="https://nayankyada.com/blog/how-i-wire-sanity-webhooks-to-nextjs-isr-revalidation-with-hmac-verification" rel="noopener noreferrer"&gt;Sanity webhooks and Next.js ISR revalidation with HMAC verification&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: the revalidation tag does not match the fetch tag
&lt;/h2&gt;

&lt;p&gt;This is the most common bug I see on inherited codebases. The webhook calls &lt;code&gt;revalidateTag&lt;/code&gt; with one string, and the fetch was cached with a different one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/sanity/fetch.ts - broken: tag names drift apart&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;client&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;./client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&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;`*[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&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="s2"&gt;`post-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/revalidate/route.ts - revalidates a tag nobody uses&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;revalidateTag&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="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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&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="nf"&gt;revalidateTag&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="nx"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "post", but fetches use "post-my-slug"&lt;/span&gt;
  &lt;span class="k"&gt;return&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="na"&gt;ok&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one naming convention used in both places. Tag by document type, and optionally by type plus slug, and have the webhook send both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/sanity/tags.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;tagsFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&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;slug&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;type&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="kd"&gt;type&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;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use &lt;code&gt;tagsFor&lt;/code&gt; in every fetch and in the route handler. Be aware that the exact signature of &lt;code&gt;revalidateTag&lt;/code&gt; has changed across recent Next.js majors, so check the Next.js 16 docs for the version you run rather than copying an older snippet. If webhooks fire in bursts, batching helps; see &lt;a href="https://nayankyada.com/blog/sanity-webhook-revalidation-batching-tags-nextjs-isr" rel="noopener noreferrer"&gt;Sanity webhook revalidation: batching tags in Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: references and parent pages are not invalidated
&lt;/h2&gt;

&lt;p&gt;An editor changes an author name or a navigation item. The author document revalidates, but the 200 blog posts that embed that author stay cached. Either tag every query with the referenced types it reads, or send the webhook a list of affected documents using a GROQ projection that follows references. Without this, the site looks randomly stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: a cache layer in front of Next.js
&lt;/h2&gt;

&lt;p&gt;If Next.js revalidates correctly but visitors still see old HTML, look upstream. A CDN, a proxy, or an overly aggressive &lt;code&gt;Cache-Control&lt;/code&gt; header can pin the page. Compare the response headers from the origin and from the public domain. If they differ, that is your answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: the client uses the CDN API for reads that need freshness
&lt;/h2&gt;

&lt;p&gt;The Sanity API CDN can serve slightly older data. For server-side fetches triggered by a webhook, the content may not have propagated yet when you revalidate. Using the live API without the CDN for those server fetches avoids the race. Confirm the option name in the current &lt;code&gt;@sanity/client&lt;/code&gt; documentation before changing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify the fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Publish a small change in Studio, such as a headline edit.&lt;/li&gt;
&lt;li&gt;Check the webhook delivery log and confirm a 200 response.&lt;/li&gt;
&lt;li&gt;Reload the public page in a private window and confirm the change.&lt;/li&gt;
&lt;li&gt;Repeat with a referenced document, such as an author, and confirm dependent pages update.&lt;/li&gt;
&lt;li&gt;Check response headers on the public domain for unexpected caching.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add a lightweight log line in the route handler that records the document type, slug, and tags revalidated. When an editor reports a problem next month, you can answer in a minute instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How do you name cache tags, and where is that convention documented?&lt;/li&gt;
&lt;li&gt;What happens when a referenced document changes?&lt;/li&gt;
&lt;li&gt;Can I see the webhook delivery log and the route handler code?&lt;/li&gt;
&lt;li&gt;How do you test revalidation before launch?&lt;/li&gt;
&lt;li&gt;Will you fix it without disabling caching?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good answer names a tag convention and a test. A bad answer is "we set revalidate to 10 seconds."&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I do
&lt;/h2&gt;

&lt;p&gt;Diagnosing and fixing stale-content problems on Sanity and Next.js sites is work I do regularly, usually as part of a &lt;a href="https://dev.to/services#packages"&gt;Performance and Sanity audit from $800&lt;/a&gt;. If your editors have stopped trusting the publish button, that is a good place to start.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>nextjs</category>
      <category>isr</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Next.js build taking too long on a Sanity site: causes and the fix</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 06 Nov 2026 12:19:22 +0000</pubDate>
      <link>https://dev.to/nayankyada/nextjs-build-taking-too-long-on-a-sanity-site-causes-and-the-fix-5aid</link>
      <guid>https://dev.to/nayankyada/nextjs-build-taking-too-long-on-a-sanity-site-causes-and-the-fix-5aid</guid>
      <description>&lt;p&gt;Your Next.js build is taking too long when a deploy that once finished in a couple of minutes now runs for fifteen, twenty, or longer, and a content change or a one-line fix sits in a queue. On Sanity sites this is almost always self-inflicted: the build is doing the work of rendering and fetching every page in the dataset, every time. Here is how I diagnose it, what it costs you to ignore, and the order I fix it in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms that point to a build problem
&lt;/h2&gt;

&lt;p&gt;Check whether these sound familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy time grows every time the editors publish more content.&lt;/li&gt;
&lt;li&gt;The build log shows the same page count climbing, with long gaps between prerendered routes.&lt;/li&gt;
&lt;li&gt;Builds occasionally fail with timeouts or fetch errors from the Sanity API, then pass on retry.&lt;/li&gt;
&lt;li&gt;Editors ask whether the site is broken because they cannot see their change for ages.&lt;/li&gt;
&lt;li&gt;Your hosting plan reports a lot of build minutes, and a preview deploy per pull request multiplies it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If build time scales with content count, the cause is structural, not a bad server. Throwing a bigger build machine at it helps a little and hides the real issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ignoring it costs
&lt;/h2&gt;

&lt;p&gt;The direct cost is build minutes, which is a line item on your hosting bill (see my breakdown of &lt;a href="https://nayankyada.com/blog/vercel-bill-too-high-on-a-nextjs-site-what-drives-it-and-how-to-cut-it" rel="noopener noreferrer"&gt;what drives a Vercel bill on a Next.js site&lt;/a&gt;). The bigger cost is behavioural. When deploys are slow, developers batch changes, hotfixes wait, and marketing stops trusting that publishing will show up. A site where nobody wants to deploy is a site that rots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: prerendering every document on every build
&lt;/h2&gt;

&lt;p&gt;The most common pattern I find is a dynamic route that returns every slug from &lt;code&gt;generateStaticParams&lt;/code&gt;. With 40 pages that is fine. With 4,000 it is the entire build.&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 (broken: prerenders everything)&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;client&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;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&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;generateStaticParams&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;slugs&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == 'post' &amp;amp;&amp;amp; defined(slug.current)].slug.current`&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;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to prerender only what matters and let the rest build on demand. Pages not returned from &lt;code&gt;generateStaticParams&lt;/code&gt; are rendered on first request and then cached, because &lt;code&gt;dynamicParams&lt;/code&gt; defaults to true.&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 (fixed: top pages only, rest on demand)&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;client&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;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;generateStaticParams&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;slugs&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == 'post' &amp;amp;&amp;amp; defined(slug.current)]
      | order(publishedAt desc)[0...50].slug.current`&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;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifty of the newest posts is a starting point, not a rule. Use your analytics: prerender the pages that actually receive traffic. I use the same approach in the &lt;a href="https://nayankyada.com/blog/how-i-build-a-nextjs-blog-that-stays-fast-at-100-posts" rel="noopener noreferrer"&gt;100+ post blog build&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: every page fetches far more than it renders
&lt;/h2&gt;

&lt;p&gt;The second pattern is a page query that pulls whole documents, dereferences everything, and returns fields the template never touches. Multiply that payload by thousands of pages and the build spends its life waiting on the network.&lt;/p&gt;

&lt;p&gt;Open your page queries and look for a bare projection that returns everything, or deep reference expansion several levels down. Replace it with an explicit projection of the fields the page uses. The techniques in &lt;a href="https://nayankyada.com/blog/advanced-groq-queries-joins-coalesce-and-array-flattening-in-sanity" rel="noopener noreferrer"&gt;advanced GROQ queries&lt;/a&gt; and the &lt;a href="https://nayankyada.com/blog/how-i-handle-conditional-groq-projections-to-cut-query-payload-by-60" rel="noopener noreferrer"&gt;conditional projections post&lt;/a&gt; cover this in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: the same data fetched many times
&lt;/h2&gt;

&lt;p&gt;Layouts, headers, footers, and navigation often each call Sanity separately, and so does every page that needs site settings. Some of these are deduplicated within a render, some are not, depending on how you fetch. A site with a navigation query, a settings query, and a footer query on each of 2,000 pages is 6,000 requests before the page content itself.&lt;/p&gt;

&lt;p&gt;Give shared data one fetch function with explicit caching, and tag it so you can invalidate it precisely:&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;// sanity/lib/fetch.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;client&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;./client&lt;/span&gt;&lt;span class="dl"&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;sanityFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;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;params&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="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;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&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;Then call it with a tag such as &lt;code&gt;settings&lt;/code&gt; for global data. Pair it with webhook-driven invalidation, as described in &lt;a href="https://nayankyada.com/blog/sanity-webhook-revalidation-batching-tags-nextjs-isr" rel="noopener noreferrer"&gt;batching tags on Sanity webhooks&lt;/a&gt;, so a one-hour fallback is only a safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: the build is not using the CDN
&lt;/h2&gt;

&lt;p&gt;For published content during a build, the Sanity API CDN is usually the right choice. If your client is configured with the CDN turned off everywhere, builds hit the live API directly and are more exposed to rate limits, which explains the random failures that pass on retry. Keep the CDN on for published reads and turn it off only for draft or preview fetches. Confirm the exact behaviour in your &lt;code&gt;next-sanity&lt;/code&gt; client config and the Sanity docs for your version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: heavy work done at build time
&lt;/h2&gt;

&lt;p&gt;Look for things that do not belong in the build at all: generating image variants for every asset, rendering OG images for every page ahead of time, or running a search index sync inside the build step. Move these to a webhook-triggered job or generate them lazily on request. A build should render pages, not run a content pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix order I use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Measure first: note total build time and page count from a recent deploy log.&lt;/li&gt;
&lt;li&gt;Cap &lt;code&gt;generateStaticParams&lt;/code&gt; to the pages that matter. This is usually the biggest single win.&lt;/li&gt;
&lt;li&gt;Trim page queries to explicit projections.&lt;/li&gt;
&lt;li&gt;Centralise shared fetches with tags and a revalidate window.&lt;/li&gt;
&lt;li&gt;Check CDN usage and rate-limit errors.&lt;/li&gt;
&lt;li&gt;Move non-rendering work out of the build.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do them in that order, rebuilding after each, so you know which change paid off.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify the fix
&lt;/h2&gt;

&lt;p&gt;Compare build duration and the number of statically generated routes before and after. Then visit a page you did not prerender: the first load should work, and the second should be served from cache. Finally, publish a change in Studio and confirm it appears without a full redeploy. If it does not, the problem has moved to revalidation, not the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Which pages are prerendered, and why those?&lt;/li&gt;
&lt;li&gt;How are shared queries cached, and how are they invalidated on publish?&lt;/li&gt;
&lt;li&gt;What happens on first request to a page that was not prerendered?&lt;/li&gt;
&lt;li&gt;Can you show the build log before and after?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answers are vague, expect the same problem to return as content grows.&lt;/p&gt;

&lt;p&gt;This is work I do. If your deploys have gotten painfully slow, my &lt;a href="https://dev.to/services#packages"&gt;Performance &amp;amp; Sanity audit, from $800&lt;/a&gt;, finds the cause and gives you a prioritised fix list.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sanitycms</category>
      <category>isr</category>
      <category>performance</category>
    </item>
    <item>
      <title>Restructure a Sanity schema on a live site without breaking pages</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 05 Nov 2026 12:36:02 +0000</pubDate>
      <link>https://dev.to/nayankyada/restructure-a-sanity-schema-on-a-live-site-without-breaking-pages-1p4c</link>
      <guid>https://dev.to/nayankyada/restructure-a-sanity-schema-on-a-live-site-without-breaking-pages-1p4c</guid>
      <description>&lt;p&gt;If you need to restructure a Sanity schema on a site that is already live, the fear is reasonable: one renamed field and a page renders blank, or an editor opens a document and half the content has vanished. The good news is that Sanity stores content separately from your schema, so you can change the model safely if you move in the right order. This post is the order I use on inherited and aging projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms of a content model that needs restructuring
&lt;/h2&gt;

&lt;p&gt;You rarely hear "our schema is bad". You hear things like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editors paste the same block of copy into five documents because there is no shared type.&lt;/li&gt;
&lt;li&gt;Pages have fields named &lt;code&gt;heroTitle2&lt;/code&gt;, &lt;code&gt;heroTitleNew&lt;/code&gt; and &lt;code&gt;heroTitleOld&lt;/code&gt;, and nobody knows which one renders.&lt;/li&gt;
&lt;li&gt;A simple page query is forty lines of GROQ with nested conditionals, because three generations of structure all still exist in the dataset.&lt;/li&gt;
&lt;li&gt;Developers are afraid to touch the Studio config, so new requests get bolted on instead of modelled.&lt;/li&gt;
&lt;li&gt;Everything is a single &lt;code&gt;page&lt;/code&gt; type with thirty optional fields, so editors see a wall of inputs that mostly do not apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not cosmetic. They slow every future change, they make queries heavier than they should be, and they are a large part of why editors end up avoiding the Studio. I covered that side in &lt;a href="https://nayankyada.com/blog/sanity-studio-editor-experience-why-editors-avoid-it-and-the-fix" rel="noopener noreferrer"&gt;why editors avoid Sanity Studio and the fix&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to leave it alone
&lt;/h2&gt;

&lt;p&gt;The cost is not a single bill. It is a tax on every task. A new landing page takes three days instead of one because the developer has to work out which fields are live. Bugs appear when someone fills in the legacy field and the frontend reads the new one. Query payloads grow, which shows up in page speed and in your API usage. And the person who understands the mess becomes a single point of failure. If that person is already gone, start with &lt;a href="https://nayankyada.com/blog/take-over-an-existing-nextjs-project-after-your-developer-disappears" rel="noopener noreferrer"&gt;taking over an existing Next.js project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: audit what is actually used
&lt;/h2&gt;

&lt;p&gt;Before changing anything, find out what the dataset contains, not what the schema claims. Export a copy with &lt;code&gt;npx sanity@latest dataset export&lt;/code&gt; and look at which document types and fields are populated. Count documents per type and check which fields are empty across nearly all documents. Fields that are never filled in can usually be deleted outright, with no migration.&lt;/p&gt;

&lt;p&gt;Then do the reverse check: search the frontend for every field a query reads. Anything in the schema that no query touches is a candidate for removal. My process for finding dead types is in &lt;a href="https://nayankyada.com/blog/how-i-audit-and-prune-unused-sanity-document-types-to-reclaim-studio-performance" rel="noopener noreferrer"&gt;auditing and pruning unused Sanity document types&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Write the target model on paper first. Decide which types become reusable objects, which become referenced documents, and which fields collapse into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: work somewhere that is not production
&lt;/h2&gt;

&lt;p&gt;Do not rehearse a restructure on the dataset editors are using. Export production into a separate dataset or a staging project, point a preview deployment at it, and run everything there first. If you only have one dataset today, &lt;a href="https://nayankyada.com/blog/sanity-staging-and-production-on-one-dataset-how-to-split-it-safely" rel="noopener noreferrer"&gt;splitting staging and production safely&lt;/a&gt; is the prerequisite step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: make the frontend read both shapes
&lt;/h2&gt;

&lt;p&gt;This is the step that prevents downtime. Before any data moves, update your GROQ to accept old and new fields. Deploy that first. Then the data can change underneath without the site noticing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// lib/sanity/queries.ts - transition query, safe before and after migration
*[_type == "page" &amp;amp;&amp;amp; slug.current == $slug][0]{
  _id,
  title,
  "hero": {
    "title": coalesce(hero.title, heroTitle),
    "subtitle": coalesce(hero.subtitle, heroSubtitle)
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;coalesce&lt;/code&gt; returns the first value that is not null, so the page renders correctly whether a document has the old flat fields, the new &lt;code&gt;hero&lt;/code&gt; object, or both. For more patterns like this, see &lt;a href="https://nayankyada.com/blog/advanced-groq-queries-joins-coalesce-and-array-flattening-in-sanity" rel="noopener noreferrer"&gt;advanced GROQ queries with joins and coalesce&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: add the new fields, keep the old ones temporarily
&lt;/h2&gt;

&lt;p&gt;In the Studio, add the new structure alongside the old. Mark the old fields as &lt;code&gt;hidden: true&lt;/code&gt; or &lt;code&gt;readOnly: true&lt;/code&gt; so editors stop writing to them, but do not delete them yet. If you delete a field from the schema while documents still hold values, Sanity shows those values as unknown fields in the editor, and your migration script becomes harder to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: migrate the data with a script
&lt;/h2&gt;

&lt;p&gt;Sanity has a migration tool in the CLI. You create a migration with &lt;code&gt;npx sanity@latest migration create&lt;/code&gt;, write the transform, and run it with &lt;code&gt;npx sanity@latest migration run&lt;/code&gt;. It runs as a dry run by default, and you pass &lt;code&gt;--no-dry-run&lt;/code&gt; to commit changes. Always read the dry-run output first.&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;// migrations/hero-fields-to-object/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;defineMigration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unset&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;sanity/migrate&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;defineMigration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Move flat hero fields into a hero object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;documentTypes&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;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heroTitle&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heroTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heroSubtitle&lt;/span&gt;&lt;span class="p"&gt;})),&lt;/span&gt;
        &lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heroTitle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heroSubtitle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;unset&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two habits matter here. First, make the script idempotent: the early return means running it twice does nothing the second time. Second, run it against the staging dataset, open a sample of migrated documents in the Studio, and load the preview pages before touching production.&lt;/p&gt;

&lt;p&gt;Note that drafts and published versions are separate documents. Check that your migration handles both, otherwise an editor opens a draft next week and finds the old structure back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: clean up
&lt;/h2&gt;

&lt;p&gt;Once production is migrated and the site has run cleanly for a while, remove the old fields from the schema and simplify the query back to the new fields only. Regenerate types if you use TypeGen so the compiler flags anything still reading a removed field. That is the point where the dual-read GROQ goes away and the payload shrinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer before they start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Will you run the migration on a copy of the dataset first, and can I see the dry-run output?&lt;/li&gt;
&lt;li&gt;How will the live site keep rendering while data moves?&lt;/li&gt;
&lt;li&gt;What is the rollback plan? A dataset export taken immediately before the run is the minimum answer.&lt;/li&gt;
&lt;li&gt;How will you handle draft documents and references that point at changed types?&lt;/li&gt;
&lt;li&gt;Who writes the editor-facing explanation of what changed in the Studio?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A developer who answers these without hesitation has done this before. One who proposes editing production schema and "seeing what happens" has not.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to get help
&lt;/h2&gt;

&lt;p&gt;A restructure on a small site is a few days of careful work. On a dataset with thousands of documents, multiple locales and several frontends reading it, it deserves a proper plan. This is work I do: I usually begin with the &lt;a href="https://dev.to/services#packages"&gt;performance and Sanity audit from $800&lt;/a&gt;, which produces the target model and a migration plan, before any data is touched.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>sanitystudio</category>
      <category>groq</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Vercel bill too high on a Next.js site? What drives it and how to cut it</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:21:34 +0000</pubDate>
      <link>https://dev.to/nayankyada/vercel-bill-too-high-on-a-nextjs-site-what-drives-it-and-how-to-cut-it-3cc6</link>
      <guid>https://dev.to/nayankyada/vercel-bill-too-high-on-a-nextjs-site-what-drives-it-and-how-to-cut-it-3cc6</guid>
      <description>&lt;p&gt;If your Vercel bill is too high for a Next.js site that does not get much traffic, the problem is almost never the traffic. It is a handful of configuration choices that make every request, crawl, and content edit cost more than it should. This post walks through the usual culprits on Sanity-backed sites, how to confirm each one, and the order I would fix them in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms that point to a config problem, not growth
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent. Usage climbs while analytics visitors stay flat. A month with one big content import costs more than a month with a campaign. The invoice has line items nobody on the team can explain. If any of that sounds familiar, you are paying for waste.&lt;/p&gt;

&lt;p&gt;Before touching code, open the Usage page in the Vercel dashboard and note which metric is largest: data transfer, edge requests, function duration and invocations, image optimization, or ISR reads and writes. Plan names and included amounts change, so confirm limits on Vercel's pricing page rather than trusting any blog, including mine. For background on how the plans fit together, see &lt;a href="https://nayankyada.com/blog/vercel-pricing-2026-what-you-actually-pay-for-a-real-nextjs-project" rel="noopener noreferrer"&gt;what you actually pay for a real Next.js project on Vercel&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to ignore
&lt;/h2&gt;

&lt;p&gt;Usage-based costs compound quietly. A site that costs a modest amount at launch can drift upward every month as the content library grows, more pages get crawled, and more editors publish. Teams then react by downgrading features or blaming the framework, when the fix is usually a day or two of targeted work. Ignoring it also hides risk: the same settings that inflate cost often cause slow pages and stale content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: image optimization on oversized originals
&lt;/h2&gt;

&lt;p&gt;Every unique combination of source image, width, and quality is a transformation that gets cached and billed in some form. Sanity images are often uploaded at 4000 pixels wide straight from a designer, and a careless sizes attribute makes Next generate many variants of each.&lt;/p&gt;

&lt;p&gt;Sanity's CDN already resizes and converts formats on the fly. Running those images through Vercel's optimizer as well means you pay twice for the same job. I covered the quality side of that in &lt;a href="https://nayankyada.com/blog/how-i-serve-webp-and-avif-from-sanity-without-double-encoding-in-nextjs" rel="noopener noreferrer"&gt;serving WebP and AVIF from Sanity without double-encoding&lt;/a&gt;. The cost fix is to hand resizing to Sanity with a custom loader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/sanity-image-loader.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ImageLoaderProps&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/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanityImageLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ImageLoaderProps&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;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;auto&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;format&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&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;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;loaderFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./lib/sanity-image-loader.ts&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to every next/image on the site, so if you also serve non-Sanity images, either pass full URLs your loader can handle or set the loader per component. Also fix the sizes attribute, so the browser asks for 400 pixels on a card instead of 1600.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: revalidation that rebuilds too much
&lt;/h2&gt;

&lt;p&gt;A webhook that calls a path revalidation for the homepage, the blog index, and every listing on each publish creates a burst of regenerations. Ten editors saving drafts all day makes it worse if drafts trigger the webhook at all.&lt;/p&gt;

&lt;p&gt;The fix is to filter the webhook so it fires only on published documents, and revalidate by cache tag so only affected pages regenerate. I wrote up the batching approach in &lt;a href="https://nayankyada.com/blog/sanity-webhook-revalidation-batching-tags-nextjs-isr" rel="noopener noreferrer"&gt;Sanity webhook revalidation: batching tags&lt;/a&gt;. Confirm the problem by looking at function invocations right after an editing session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: the proxy runs on everything
&lt;/h2&gt;

&lt;p&gt;In Next.js 16 the old middleware file is the proxy file. If its matcher is missing or too broad, it executes on every request: images, fonts, scripts, and bot probes included. That inflates edge requests and adds latency to pages that could have been served straight from cache.&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;// proxy.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// locale or redirect logic only&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!_next/static|_next/image|favicon.ico|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;..*).*)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better still, move redirects into next.config.ts where they are static, and keep the proxy for logic that genuinely needs per-request code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: bots and dynamic routes
&lt;/h2&gt;

&lt;p&gt;Crawlers hit your site constantly. If a route reads cookies, headers, or search params, or uses an uncached fetch, the whole page becomes dynamic and every bot visit runs a function. Faceted filters and calendar-style URLs can give a crawler effectively infinite pages. Check Observability for the top paths and user agents. Typical fixes: make routes static with generateStaticParams, push filtering into client state instead of URL variants, block junk paths in robots.txt, and use Vercel's firewall rules for abusive traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: over-fetching from the CMS
&lt;/h2&gt;

&lt;p&gt;Fat GROQ queries and fetches without cache tags make pages slower to render, which raises function duration, and they also drive up your Sanity bill. If both invoices are rising, read &lt;a href="https://nayankyada.com/blog/sanity-api-bill-spiking-why-requests-explode-on-nextjs-sites" rel="noopener noreferrer"&gt;why Sanity API requests explode on Next.js sites&lt;/a&gt; next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix order I would follow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the Usage page and pick the single biggest line item.&lt;/li&gt;
&lt;li&gt;Set a spend limit so a runaway month cannot surprise you.&lt;/li&gt;
&lt;li&gt;Fix images first, since it is usually the cheapest win.&lt;/li&gt;
&lt;li&gt;Tighten the proxy matcher and static redirects.&lt;/li&gt;
&lt;li&gt;Filter webhooks and move to tag-based revalidation.&lt;/li&gt;
&lt;li&gt;Audit dynamic routes and bot traffic.&lt;/li&gt;
&lt;li&gt;Recheck usage after one full week, not one day.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;p&gt;Ask which metric they would attack first and why. Ask how they will verify the saving, and whether they will show before-and-after usage from the dashboard. Be wary of anyone who proposes moving hosts before they have looked at the usage breakdown. A move can be right, but the same waste usually follows you. The hosting comparison in &lt;a href="https://nayankyada.com/blog/nextjs-hosting-cost-in-2026-vercel-vs-netlify-vs-railway-vs-vps" rel="noopener noreferrer"&gt;Next.js hosting cost across Vercel, Netlify, Railway and a VPS&lt;/a&gt; helps judge when it genuinely is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I do
&lt;/h2&gt;

&lt;p&gt;Tracing a bloated Vercel or Sanity bill back to its causes is work I do as part of a fixed-price performance and Sanity audit, from $800. Details are on the &lt;a href="https://dev.to/services#packages"&gt;services page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>isr</category>
      <category>imageoptimization</category>
    </item>
    <item>
      <title>Next.js pages not indexed by Google: causes and the fix path</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:08:51 +0000</pubDate>
      <link>https://dev.to/nayankyada/nextjs-pages-not-indexed-by-google-causes-and-the-fix-path-3an3</link>
      <guid>https://dev.to/nayankyada/nextjs-pages-not-indexed-by-google-causes-and-the-fix-path-3an3</guid>
      <description>&lt;p&gt;Your developer launched the Next.js and Sanity site, the pages look fine, and Google still shows half of them as "Discovered - currently not indexed" or "Crawled - currently not indexed". If you are searching for why your Next.js pages are not indexed, the cause is almost always one of five fixable things, and none of them require a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the symptoms look like
&lt;/h2&gt;

&lt;p&gt;Open Search Console and go to the page indexing report. The reason attached to each URL is the most useful clue you have. These are the patterns I see most often on Sanity-backed sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages excluded by a noindex tag, even though nobody meant to add one.&lt;/li&gt;
&lt;li&gt;Alternate page with proper canonical tag, where Google thinks your page is a copy of another one.&lt;/li&gt;
&lt;li&gt;Discovered or Crawled but not indexed, where Google found the URL and chose not to keep it.&lt;/li&gt;
&lt;li&gt;Soft 404, where an empty or error-like page returns a normal status.&lt;/li&gt;
&lt;li&gt;Submitted URL not found in the sitemap report, where the sitemap lists URLs that no longer exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic that never arrives is a quiet loss. Nobody gets an alert. The cost is every month those pages could have ranked, plus the money spent building them. If the site replaced an older one, the loss is compounded, because the old URLs were ranking and the new ones are not. The &lt;a href="https://nayankyada.com/blog/website-redesign-without-losing-seo-a-sanity-nextjs-checklist" rel="noopener noreferrer"&gt;website redesign SEO checklist&lt;/a&gt; covers the redirect side of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: a noindex that should not be there
&lt;/h2&gt;

&lt;p&gt;This is the first thing to rule out. Common sources: a shared metadata helper that sets robots to noindex when an environment variable is missing, a Sanity SEO field where editors tick a "hide from search" box on templates and the default leaks to every page, or a staging flag copied into the production environment on Vercel.&lt;/p&gt;

&lt;p&gt;Do not trust the code. View source on the live URL and search for the robots meta tag. Also check the response headers for an X-Robots-Tag, which can be set in next.config or at the host level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: canonicals pointing somewhere else
&lt;/h2&gt;

&lt;p&gt;If every page has the canonical set to the homepage, or to a preview domain, Google will drop the page. This happens when the canonical is built from a base URL environment variable that is wrong in production, or when a trailing slash difference makes the canonical not match the served URL.&lt;/p&gt;

&lt;p&gt;Build canonicals from one source of truth and make them self-referencing per page. Here is the pattern I use in the App Router:&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/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&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&lt;/span&gt;&lt;span class="dl"&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;notFound&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/navigation&lt;/span&gt;&lt;span class="dl"&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;client&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;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

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

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;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;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&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;Metadata&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&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;client&lt;/span&gt;&lt;span class="p"&gt;.&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;`*[_type == "page" &amp;amp;&amp;amp; slug.current == $slug][0]{ title, "description": seo.description, "noIndex": seo.noIndex }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;robots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noIndex&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;follow&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="kc"&gt;undefined&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;Note that robots is only set when the editor explicitly opts out. Leaving it undefined means the default, which is indexable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: the sitemap lists the wrong things
&lt;/h2&gt;

&lt;p&gt;A sitemap that includes redirected URLs, noindexed pages, or drafts tells Google the site is untidy. A sitemap that is missing whole content types means Sanity documents never get discovered except through internal links. Generate it from Sanity, exclude opted-out documents, and use the real last-modified date:&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/sitemap.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&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&lt;/span&gt;&lt;span class="dl"&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;client&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;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

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

&lt;span class="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;sitemap&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;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sitemap&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;pages&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == "page" &amp;amp;&amp;amp; defined(slug.current) &amp;amp;&amp;amp; seo.noIndex != true]{ "slug": slug.current, "updated": _updatedAt }`&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;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated&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;Then submit the sitemap URL in Search Console and compare submitted against indexed counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: soft 404s and empty pages
&lt;/h2&gt;

&lt;p&gt;If a Sanity document exists but its body is empty, or a dynamic route renders a "not found" message without calling notFound(), Google sees a thin page and reports a soft 404. Also be aware that when a page streams, the status code can already be sent as 200 before a not-found is triggered, and Next.js handles this by adding a noindex tag to the output. Check what your error pages actually return with a header check rather than assuming.&lt;/p&gt;

&lt;p&gt;On the content side, add a Studio validation rule so editors cannot publish a page with no title, description, or body. That prevents thin pages at the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: weak internal linking and duplicate content
&lt;/h2&gt;

&lt;p&gt;"Crawled - currently not indexed" is often a quality signal rather than a technical fault. Pages that exist only in the sitemap with no links from other pages, near-identical location or service pages, and tag archives with one post each all land here. The fix is editorial plus structural: link important pages from navigation and related content, merge or remove near-duplicates, and give each page a clear reason to exist. Adding proper &lt;a href="https://nayankyada.com/blog/nextjs-seo-adding-json-ld-structured-data-with-schemaorg-schemas" rel="noopener noreferrer"&gt;JSON-LD structured data&lt;/a&gt; helps Google understand the page, but it will not rescue a thin one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix path and how to verify
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export the not-indexed URLs from Search Console and group them by reason.&lt;/li&gt;
&lt;li&gt;Check the live HTML and headers of three URLs per group for robots, canonical, and status.&lt;/li&gt;
&lt;li&gt;Fix the shared cause in code (metadata helper, sitemap, environment variables) rather than page by page.&lt;/li&gt;
&lt;li&gt;Fix content causes in Sanity: thin pages, missing SEO fields, duplicates.&lt;/li&gt;
&lt;li&gt;Redeploy, use the URL inspection tool on one URL per group, then request indexing.&lt;/li&gt;
&lt;li&gt;Recheck the report after a couple of weeks. Counts should move toward the sitemap total.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are not sure whether the problem is technical or editorial, a &lt;a href="https://nayankyada.com/blog/nextjs-website-audit-what-a-paid-one-covers-and-how-to-judge-it" rel="noopener noreferrer"&gt;Next.js website audit&lt;/a&gt; answers that quickly. Choosing a stack with this in mind from the start is covered in &lt;a href="https://nayankyada.com/blog/how-to-choose-a-cms-for-seo-rendering-structured-data-and-core-web-vitals" rel="noopener noreferrer"&gt;how to choose a CMS for SEO&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Can you show me the rendered robots tag, canonical, and status code for five live URLs?&lt;/li&gt;
&lt;li&gt;Where does the base URL come from, and is it set correctly in production?&lt;/li&gt;
&lt;li&gt;How is the sitemap generated, and does it exclude noindexed and redirected pages?&lt;/li&gt;
&lt;li&gt;What happens in Studio when an editor publishes a page with an empty SEO field?&lt;/li&gt;
&lt;li&gt;Will you give me a before-and-after of the indexing report?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A developer who answers these with evidence, not reassurance, is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I do
&lt;/h2&gt;

&lt;p&gt;Diagnosing and fixing indexing problems on Sanity and Next.js sites is work I do as part of a Performance &amp;amp; Sanity audit, which starts from $800. You can see the details on the &lt;a href="https://dev.to/services#packages"&gt;services page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>approuter</category>
      <category>sanitycms</category>
    </item>
    <item>
      <title>Sanity staging and production on one dataset: how to split it safely</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Mon, 02 Nov 2026 12:43:18 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-staging-and-production-on-one-dataset-how-to-split-it-safely-33ne</link>
      <guid>https://dev.to/nayankyada/sanity-staging-and-production-on-one-dataset-how-to-split-it-safely-33ne</guid>
      <description>&lt;p&gt;If your Sanity staging and production environments read from the same dataset, you are one bad script or one half-finished edit away from a public mistake. I see this setup on inherited projects constantly: a Vercel preview URL that looks like staging but is wired to the live dataset. This post covers the symptoms, what it costs to ignore, and the safe path to a proper split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms that you share one dataset
&lt;/h2&gt;

&lt;p&gt;You probably have this problem if any of these sound familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editors are afraid to create a draft because "it might show up on the site".&lt;/li&gt;
&lt;li&gt;The developer tests a schema change by editing live documents.&lt;/li&gt;
&lt;li&gt;The staging site and the production site show identical content, always.&lt;/li&gt;
&lt;li&gt;There is one &lt;code&gt;.env&lt;/code&gt; file, or the preview and production environment variables in Vercel have the same dataset value.&lt;/li&gt;
&lt;li&gt;Nobody can say which documents are test data. Titles like "test 2" or "DELETE ME" are live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check it in two minutes. Open your Vercel project settings, compare the Sanity dataset variable across the Production and Preview environments, and open the Studio config to see which dataset it points to. If they all say &lt;code&gt;production&lt;/code&gt;, that is your answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to ignore
&lt;/h2&gt;

&lt;p&gt;The risk is not abstract. A shared dataset means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Migration scripts run against live content.&lt;/strong&gt; A patch script with a wrong filter rewrites real pages. Sanity has history, but restoring document by document under pressure is slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema changes land before the code is ready.&lt;/strong&gt; You rename a field in the Studio, editors fill it in, and the production frontend still queries the old name. Pages render blank sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No safe place to rehearse a redesign or a migration.&lt;/strong&gt; Every big change is tested in front of your customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview deployments leak.&lt;/strong&gt; Draft content for a launch can be visible through a preview URL that someone shares in Slack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One broken hero on your homepage costs more than the afternoon it takes to fix this properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix path
&lt;/h2&gt;

&lt;p&gt;The safe order is: back up, create the new dataset, copy content, change config, then change the workflow. Do not start by editing environment variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Back up production first
&lt;/h3&gt;

&lt;p&gt;Export before touching anything. The archive includes documents and assets.&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="c"&gt;# run from your Studio folder&lt;/span&gt;
npx sanity dataset &lt;span class="nb"&gt;export &lt;/span&gt;production ./backups/production-before-split.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store that file somewhere outside the repo. It is your undo button.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a staging dataset and seed it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx sanity dataset create staging
npx sanity dataset import ./backups/production-before-split.tar.gz staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer, the CLI also has a &lt;code&gt;dataset copy&lt;/code&gt; command that does this in one step. Either way, open the new dataset in the Studio afterwards and confirm that images load, because assets are the part that most often goes wrong on a first attempt.&lt;/p&gt;

&lt;p&gt;A word on privacy: a copy of production content may contain real customer data such as form submissions or author emails. If so, scrub those documents in staging before giving anyone access.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Expose both datasets in the Studio
&lt;/h3&gt;

&lt;p&gt;Editors need to know which environment they are in. Workspaces make that obvious, with a separate URL path and name for each.&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;// sanity.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;defineConfig&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;sanity&lt;/span&gt;&lt;span class="dl"&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;structureTool&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;sanity/structure&lt;/span&gt;&lt;span class="dl"&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;schemaTypes&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;./schemaTypes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&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;SANITY_STUDIO_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;structureTool&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;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;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schemaTypes&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&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;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most teams give editors the production workspace only and keep staging for developers and for reviewing big changes. If you want editors to rehearse, tell them clearly that nothing in staging reaches the live site.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Drive the dataset from environment variables
&lt;/h3&gt;

&lt;p&gt;The frontend should never hardcode a dataset name. Read it from the environment so Vercel can scope it per environment.&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;// src/sanity/client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;createClient&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-sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_DATASET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Vercel, set &lt;code&gt;NEXT_PUBLIC_SANITY_DATASET&lt;/code&gt; to &lt;code&gt;production&lt;/code&gt; for the Production environment and &lt;code&gt;staging&lt;/code&gt; for Preview. Redeploy, because public variables are inlined at build time.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Fix everything else that points at the old setup
&lt;/h3&gt;

&lt;p&gt;This is the step that gets skipped, and it is why staging "works" but behaves oddly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks.&lt;/strong&gt; The revalidation webhook is configured per project with its own dataset filter. Create a separate one for staging that calls your preview URL, with its own secret. Otherwise staging edits revalidate production, or nothing revalidates at all. See my notes on &lt;a href="https://nayankyada.com/blog/sanity-webhook-revalidation-batching-tags-nextjs-isr" rel="noopener noreferrer"&gt;Sanity webhook revalidation with batched tags&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API tokens.&lt;/strong&gt; Read tokens used for draft mode should be separate for each environment, so a leaked preview token cannot read production drafts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS origins.&lt;/strong&gt; Add your staging and preview origins in the project settings so the Studio and any client-side fetches work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview and draft mode.&lt;/strong&gt; Point the Presentation or preview setup at the right dataset, as covered in &lt;a href="https://nayankyada.com/blog/sanity-draft-mode-preview-nextjs-app-router-vercel-edge-config" rel="noopener noreferrer"&gt;Sanity draft mode preview with Next.js App Router&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to verify the split worked
&lt;/h2&gt;

&lt;p&gt;Make a deliberate change in staging, such as adding "STAGING TEST" to a page title, and publish it. It should show on the preview URL and nowhere on production. Then check that a production edit does not appear on staging. Finally run a dry-run of any migration script against staging and confirm the document counts match your expectation before it ever sees production.&lt;/p&gt;

&lt;p&gt;Also confirm that your build does not fall back to a default dataset when the environment variable is missing. A silent fallback to &lt;code&gt;production&lt;/code&gt; quietly rebuilds the original problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;p&gt;Whether you are hiring me or someone else, ask these before work starts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which datasets exist today, and which environments point at each?&lt;/li&gt;
&lt;li&gt;How will you back up production before any change, and where is the backup stored?&lt;/li&gt;
&lt;li&gt;Will webhooks, tokens and CORS be separated too, or only the dataset name?&lt;/li&gt;
&lt;li&gt;How do schema changes move from staging to production without a gap where the frontend breaks?&lt;/li&gt;
&lt;li&gt;What goes in the handover? My &lt;a href="https://nayankyada.com/blog/sanity-cms-project-handover-checklist-what-you-should-receive" rel="noopener noreferrer"&gt;Sanity project handover checklist&lt;/a&gt; lists what you should expect, and &lt;a href="https://nayankyada.com/blog/take-over-an-existing-nextjs-project-after-your-developer-disappears" rel="noopener noreferrer"&gt;taking over an existing Next.js project&lt;/a&gt; covers the wider inherited-codebase picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most sites this split is a few hours to a day, depending on how tangled the webhooks and previews are. It often falls out of a broader review, such as a &lt;a href="https://dev.to/services#packages"&gt;Performance &amp;amp; Sanity audit from $800&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is work I do: if you suspect your Sanity site is running staging and production on one dataset, see my &lt;a href="https://nayankyada.com/services" rel="noopener noreferrer"&gt;Sanity and Next.js services&lt;/a&gt; and I can scope the split for you.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>sanitystudio</category>
      <category>nextjs</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Sanity API bill spiking? Why requests explode on Next.js sites</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sun, 01 Nov 2026 11:24:44 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-api-bill-spiking-why-requests-explode-on-nextjs-sites-3nai</link>
      <guid>https://dev.to/nayankyada/sanity-api-bill-spiking-why-requests-explode-on-nextjs-sites-3nai</guid>
      <description>&lt;p&gt;If your Sanity invoice or usage warning jumped and nobody changed the content volume, the cause is almost always how the Next.js front end talks to the API. A Sanity API bill spike is rarely about editors; it is about requests per page view. This post covers the symptoms, what leaving it alone costs, and the fix path I follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms of a Sanity API bill spike
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent across sites like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A usage email or dashboard warning about API requests or CDN requests, with no matching traffic growth.&lt;/li&gt;
&lt;li&gt;Request counts that climb on days with a crawler spike or an email campaign.&lt;/li&gt;
&lt;li&gt;Pages that feel fine but the Studio or the site occasionally returns rate-limit errors.&lt;/li&gt;
&lt;li&gt;A Vercel bill that rose at the same time, because uncached pages also burn function time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open the usage section in your Sanity project management page and compare it with your hosting analytics. If API requests grow faster than page views, the site is making too many calls per visit. Plan limits and overage rules change, so confirm the current numbers on the &lt;a href="https://nayankyada.com/blog/sanity-cms-pricing-in-2026-free-plan-growth-and-when-you-need-enterprise" rel="noopener noreferrer"&gt;Sanity pricing breakdown&lt;/a&gt; and Sanity's own pricing page before you draw conclusions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to ignore it
&lt;/h2&gt;

&lt;p&gt;The direct cost is overage or a forced plan upgrade. The indirect costs are worse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow pages, because every uncached request adds a network round trip to the render. I covered the diagnosis side in &lt;a href="https://nayankyada.com/blog/sanity-website-slow-how-to-find-the-cause-and-what-a-fix-costs" rel="noopener noreferrer"&gt;Sanity website slow? How to find the cause&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Rate limiting under load, which shows up as failed builds or blank sections exactly when traffic peaks.&lt;/li&gt;
&lt;li&gt;A false belief that Sanity is expensive, which sometimes triggers a pointless migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Buying a bigger plan hides the problem until the next traffic jump. Fixing the request pattern is usually a few days of work, not a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: the client bypasses the CDN
&lt;/h2&gt;

&lt;p&gt;The most common finding is a client configured with the CDN turned off everywhere, often copied from a preview setup and never changed back.&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;// src/sanity/lib/client.ts (broken pattern)&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;createClient&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-sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_DATASET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// every published read hits the live API&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Published content does not need to be live to the millisecond. Use the CDN for it and keep a separate path for drafts.&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;// src/sanity/lib/client.ts (fixed)&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;createClient&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-sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_DATASET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&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="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;fetchPublished&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;params&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&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;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; argument matters for the next cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: no Next.js caching or tag revalidation
&lt;/h2&gt;

&lt;p&gt;If pages render dynamically on every request, each visit costs one or more Sanity calls. Bots multiply that. The fix is to cache fetches, tag them by document type, and revalidate from a webhook when content changes. The webhook side is written up in &lt;a href="https://nayankyada.com/blog/how-i-wire-sanity-webhooks-to-nextjs-isr-revalidation-with-hmac-verification" rel="noopener noreferrer"&gt;Sanity webhooks with ISR revalidation&lt;/a&gt;, and for busy publishing days see &lt;a href="https://nayankyada.com/blog/sanity-webhook-revalidation-batching-tags-nextjs-isr" rel="noopener noreferrer"&gt;batching tags&lt;/a&gt; so ten edits do not trigger ten rebuild storms.&lt;/p&gt;

&lt;p&gt;Check for accidental opt-outs too: reading cookies or headers in a layout, or passing &lt;code&gt;cache: 'no-store'&lt;/code&gt; in a shared fetch helper, makes the whole route dynamic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: draft mode and preview leaking into production
&lt;/h2&gt;

&lt;p&gt;Preview setups sometimes poll or use a non-CDN token client on every page, not just inside draft mode. Confirm that the token-bearing client is only used when draft mode is enabled and never ships to the browser. If you run live preview, scope it to the Presentation tool sessions. See &lt;a href="https://nayankyada.com/blog/how-i-handle-sanity-draft-mode-without-sacrificing-edge-performance" rel="noopener noreferrer"&gt;draft mode without sacrificing edge performance&lt;/a&gt; for the pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: heavy queries and N+1 fetching
&lt;/h2&gt;

&lt;p&gt;One page making six separate fetches, or a list component that fetches per card, multiplies every view. Collapse them into one GROQ query with a projection that returns only what the page renders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*[_type == "post" &amp;amp;&amp;amp; defined(slug.current)] | order(publishedAt desc)[0...12]{
  title,
  "slug": slug.current,
  publishedAt,
  "author": author-&amp;gt;name,
  "image": mainImage.asset-&amp;gt;{url, metadata{dimensions, lqip}}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The related reading is &lt;a href="https://nayankyada.com/blog/how-i-model-sanity-references-to-avoid-n1-queries-in-nextjs-rsc" rel="noopener noreferrer"&gt;avoiding N+1 queries with Sanity references&lt;/a&gt;. Also check &lt;code&gt;generateStaticParams&lt;/code&gt;: a build that fetches the full document for every path, repeated by several workers, can generate a surprising number of requests per deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: bots, previews and image URLs
&lt;/h2&gt;

&lt;p&gt;Crawlers hit uncached routes such as search pages, filter combinations, and paginated archives with unbounded parameters. Cache or block those, and add a sensible robots rule. For images, serving original uploads at full size inflates CDN traffic. Use &lt;code&gt;next/image&lt;/code&gt; with proper &lt;code&gt;sizes&lt;/code&gt; so the browser requests the right width instead of the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix path
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pull usage numbers from Sanity and traffic from your host, and compare the ratio over the last few weeks.&lt;/li&gt;
&lt;li&gt;Search the codebase for &lt;code&gt;useCdn: false&lt;/code&gt;, &lt;code&gt;no-store&lt;/code&gt;, and &lt;code&gt;revalidate = 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Route all published reads through one cached fetch helper with tags.&lt;/li&gt;
&lt;li&gt;Wire a webhook that revalidates by tag on publish.&lt;/li&gt;
&lt;li&gt;Merge multi-fetch pages into single projected queries.&lt;/li&gt;
&lt;li&gt;Block or cache crawler traps.&lt;/li&gt;
&lt;li&gt;Re-measure after a week. The request-per-pageview ratio should fall sharply.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How many Sanity requests does one page view cost today, and how will you measure it?&lt;/li&gt;
&lt;li&gt;Which routes are dynamic, and why?&lt;/li&gt;
&lt;li&gt;Will content still update within a minute of publishing after the caching changes?&lt;/li&gt;
&lt;li&gt;Do you keep drafts and published reads on separate clients?&lt;/li&gt;
&lt;li&gt;Can you show before and after request counts from the dashboard?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be cautious about anyone whose first answer is to switch CMS. The API pattern is the problem, not the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I do
&lt;/h2&gt;

&lt;p&gt;This kind of diagnosis and cleanup is work I do as a fixed-scope &lt;a href="https://dev.to/services#packages"&gt;Performance and Sanity audit, from $800&lt;/a&gt;, and the findings usually tell you whether a small fix or a bigger one is needed. For what an audit covers, see &lt;a href="https://nayankyada.com/blog/nextjs-website-audit-what-a-paid-one-covers-and-how-to-judge-it" rel="noopener noreferrer"&gt;what a paid Next.js audit includes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>isr</category>
    </item>
    <item>
      <title>Sanity Preview Not Working: Causes and Fixes for Next.js</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sat, 31 Oct 2026 10:29:59 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-preview-not-working-causes-and-fixes-for-nextjs-2mf1</link>
      <guid>https://dev.to/nayankyada/sanity-preview-not-working-causes-and-fixes-for-nextjs-2mf1</guid>
      <description>&lt;p&gt;Your editor clicks Presentation in the Studio and gets a blank frame, a refused connection, or a page that loads but shows published content and no edit overlays. Preview failures almost always come down to five things: framing and CORS, the enable route and secret, the draftMode cookie, the fetch not using drafts, and caching. Work through them in this order, because each one hides the next.&lt;/p&gt;

&lt;p&gt;This post assumes you already have Presentation wired up. If you do not, start with &lt;a href="https://nayankyada.com/blog/sanity-live-preview-nextjs-app-router-presentation-tool" rel="noopener noreferrer"&gt;Sanity Live Preview in Next.js App Router (Presentation)&lt;/a&gt;. This is only about diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom checklist: which failure do you have?
&lt;/h2&gt;

&lt;p&gt;Match what you see before changing code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blank frame, console says refused to display in a frame&lt;/td&gt;
&lt;td&gt;Frame headers (cause 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame loads, Studio says it cannot connect or stays on loading&lt;/td&gt;
&lt;td&gt;Missing visual editing component or origin mismatch (cause 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable route returns 401 or 400&lt;/td&gt;
&lt;td&gt;Secret or token mismatch (cause 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page loads, but shows published content&lt;/td&gt;
&lt;td&gt;Fetch ignoring drafts (cause 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works once, then goes stale&lt;/td&gt;
&lt;td&gt;Caching (cause 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content shows but no click-to-edit overlays&lt;/td&gt;
&lt;td&gt;Stega or missing component (cause 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cause 1: the page cannot be framed
&lt;/h2&gt;

&lt;p&gt;Presentation renders your site inside an iframe in the Studio. If your app sends a header that forbids framing, the browser blocks it, and you get a blank pane. This is common on sites where someone added security headers to pass an audit.&lt;/p&gt;

&lt;p&gt;The broken pattern:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/(.*)&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="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Frame-Options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DENY&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to allow your Studio origin with a Content-Security-Policy frame-ancestors directive instead, and drop the blanket DENY. Put your real Studio URL in the list, plus the local one for development:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/(.*)&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="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;frame-ancestors 'self' https://studio.example.com http://localhost:3333&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="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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to be stricter, apply the permissive policy only on preview deployments using an environment variable. Do not leave production open to every origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: origin and URL mismatches
&lt;/h2&gt;

&lt;p&gt;Two separate origin settings trip people up.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The previewUrl in the Presentation tool.&lt;/strong&gt; If it points at localhost in production Studio, or at a deployment URL that has since changed, the frame loads the wrong thing or nothing. Check the &lt;code&gt;previewUrl.origin&lt;/code&gt; value in your Studio config for the environment you are actually using.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS origins in the Sanity project.&lt;/strong&gt; Client-side queries from the frame, such as live updates, need your site origin allowed in the project's API settings in the Sanity management console. Add the exact origin, including protocol and port, and enable credentials only for origins you trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A mismatch between a www and non-www domain, or between a Vercel preview URL and your production domain, is the classic one. The browser console will show a CORS error naming the origin it rejected. Copy that string exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: the enable route and secret
&lt;/h2&gt;

&lt;p&gt;Presentation hits your draft-enable route with a secret it generated. If your route rejects it, the frame never enters draft mode. Typical reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The route handler path in your code does not match the &lt;code&gt;previewMode.enable&lt;/code&gt; path in the Studio config.&lt;/li&gt;
&lt;li&gt;The read token is missing in the environment where the route runs. Vercel has separate variables per environment, so a token set only for Production fails on Preview.&lt;/li&gt;
&lt;li&gt;The token has viewer access only, or was created for another project. It needs read access to the dataset including drafts.&lt;/li&gt;
&lt;li&gt;You are validating against a hand-rolled secret instead of letting the Sanity helper validate the request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The working route, using the &lt;code&gt;next-sanity&lt;/code&gt; helper:&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;// src/app/api/draft-mode/enable/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineEnableDraftMode&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-sanity/draft-mode&lt;/span&gt;&lt;span class="dl"&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;client&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;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineEnableDraftMode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&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;SANITY_API_READ_TOKEN&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;If this returns 401, log whether &lt;code&gt;process.env.SANITY_API_READ_TOKEN&lt;/code&gt; is defined on that deployment. It is undefined far more often than the token is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: the draftMode cookie is set but the fetch ignores it
&lt;/h2&gt;

&lt;p&gt;Draft mode only sets a cookie. It does not change what your queries return. If the page shows published content while the toolbar says draft mode is on, your fetch layer is not switching.&lt;/p&gt;

&lt;p&gt;In Next.js 16, &lt;code&gt;draftMode()&lt;/code&gt; is async. Forgetting to await it is a common bug when code is copied from older examples. The reliable pattern is &lt;code&gt;defineLive&lt;/code&gt; from &lt;code&gt;next-sanity&lt;/code&gt;, which handles perspective and token for you. If you use a plain client, switch the perspective yourself:&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;// src/sanity/lib/fetch.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;draftMode&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/headers&lt;/span&gt;&lt;span class="dl"&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;client&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;./client&lt;/span&gt;&lt;span class="dl"&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;sanityFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;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;params&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isEnabled&lt;/span&gt; &lt;span class="p"&gt;}&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;draftMode&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;client&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;useCdn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isEnabled&lt;/span&gt; &lt;span class="p"&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;SANITY_API_READ_TOKEN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;perspective&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isEnabled&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drafts&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;published&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="nx"&gt;fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here. The CDN must be off for drafts, because the CDN only serves published content. And the token must stay on the server. If a token ever reaches the browser bundle, rotate it.&lt;/p&gt;

&lt;p&gt;Also check third-party cookies. The Studio and your site are on different origins, so the draftMode cookie is set in an iframe context. Browsers that block third-party cookies can drop it. Sanity's helper sets the cookie in a way designed for this, but a hand-written route that omits the right attributes will fail in Safari and in some Chrome configurations. Test in more than one browser before blaming your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 5: caching serves stale or published output
&lt;/h2&gt;

&lt;p&gt;Pages that are statically cached will not reflect drafts. Draft mode opts the request out of the static cache, but your own cache layers do not know that. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;use cache&lt;/code&gt; function or tagged fetch wrapping your data call without keying on draft state. The published result gets served in draft mode.&lt;/li&gt;
&lt;li&gt;A CDN or proxy in front of the preview deployment caching responses that carry the draft cookie.&lt;/li&gt;
&lt;li&gt;Middleware that rewrites or redirects before the cookie is read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: when draft mode is on, bypass your cache wrapper entirely. Keep the cached path for published traffic and call the uncached path for drafts. If you run an edge setup, &lt;a href="https://nayankyada.com/blog/how-i-handle-sanity-draft-mode-without-sacrificing-edge-performance" rel="noopener noreferrer"&gt;Sanity Draft Mode Without Sacrificing Edge Performance&lt;/a&gt; covers how to keep that split without slowing visitors down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 6: stega and missing overlays
&lt;/h2&gt;

&lt;p&gt;If content updates but click-to-edit overlays are missing, stega is not active or the visual editing component is not mounted. Stega embeds invisible source data into strings so the overlay knows which field each piece of text came from.&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;// src/sanity/lib/client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&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-sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_DATASET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&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="na"&gt;stega&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;studioUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SANITY_STUDIO_URL&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;Then render the visual editing component in your root layout only when draft mode is enabled. Without it, the frame loads but nothing is clickable.&lt;/p&gt;

&lt;p&gt;The flip side: stega characters break strings you compare or pass to non-display code, such as slugs used in URLs, enum values, or JSON-LD. Strip stega for those fields, or apply stega only on the draft-mode client. If a slug lookup fails only in preview, this is the reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify the fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the enable route directly with the secret from the Presentation request in the network tab and confirm it returns a redirect, not a 401.&lt;/li&gt;
&lt;li&gt;Check the response sets a draft-mode cookie, then reload a page and confirm the toolbar or your own indicator shows draft state.&lt;/li&gt;
&lt;li&gt;Edit a field in the Studio without publishing. The frame should update without a manual refresh.&lt;/li&gt;
&lt;li&gt;Click a text element and confirm the Studio jumps to that field.&lt;/li&gt;
&lt;li&gt;Repeat in a second browser and on the deployed Preview URL, not just localhost.&lt;/li&gt;
&lt;li&gt;Open the site in a private window with no cookie and confirm visitors still see only published content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you still cannot find it after these, that is the point where an outside look is cheaper than another evening of guessing. A &lt;a href="https://dev.to/services#packages"&gt;Performance &amp;amp; Sanity audit&lt;/a&gt;, from $800, covers the preview setup alongside everything else on the site.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>draftmode</category>
      <category>approuter</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Next.js website audit: what a paid one covers and how to judge it</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:57:46 +0000</pubDate>
      <link>https://dev.to/nayankyada/nextjs-website-audit-what-a-paid-one-covers-and-how-to-judge-it-3cc6</link>
      <guid>https://dev.to/nayankyada/nextjs-website-audit-what-a-paid-one-covers-and-how-to-judge-it-3cc6</guid>
      <description>&lt;p&gt;A Next.js website audit is a paid, time-boxed review of a live site that ends in a ranked list of fixes, each with an effort estimate and an expected outcome. If you already own a Next.js site backed by Sanity and something feels off (slow pages, editors complaining, rankings drifting), an audit is the cheapest way to turn a vague worry into a plan. This post explains what you should get for your money, what real findings look like, and how to decide afterwards whether it was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Next.js website audit should cover
&lt;/h2&gt;

&lt;p&gt;A good audit has a defined scope that you can read before paying. For a Sanity and Next.js site I split it into five areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field performance.&lt;/strong&gt; Not just a Lighthouse run on my laptop. I look at real-user Core Web Vitals (LCP, INP, CLS) from Search Console or your analytics, then lab-test the templates that matter: home page, a landing page, a long article, a listing page. If you have no field data yet, that is itself a finding. The &lt;a href="https://nayankyada.com/blog/how-i-set-up-core-web-vitals-monitoring-with-vercel-analytics-and-nextjs" rel="noopener noreferrer"&gt;Core Web Vitals monitoring setup&lt;/a&gt; is usually the first thing I recommend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering and caching.&lt;/strong&gt; Which routes are static, which are dynamic, and whether that matches what the content actually needs. Many sites are accidentally dynamic because one component reads cookies or headers, which turns a page that could be cached into one that hits the origin and Sanity on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity data layer.&lt;/strong&gt; GROQ queries, schema shape, and how pages fetch content. I check for over-fetching, reference chains that cause waterfalls, and images served without the metadata needed to prevent layout shift. Patterns here are covered in &lt;a href="https://nayankyada.com/blog/how-i-structure-sanity-schemas-to-avoid-query-waterfalls-in-nextjs" rel="noopener noreferrer"&gt;structuring Sanity schemas to avoid query waterfalls&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Studio and editor experience.&lt;/strong&gt; I open the Studio the way an editor would. Cluttered document lists, unclear field names, missing validation, and drafts mixed in with live content all cost your team time every week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO plumbing.&lt;/strong&gt; Metadata, canonical URLs, sitemap, robots, structured data, redirects, and whether the rendered HTML contains the content a crawler needs.&lt;/p&gt;

&lt;p&gt;A scope that also promises security pen-testing, legal compliance, and a full rewrite estimate for a few hundred dollars is overselling. Narrow and deep beats broad and shallow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample findings from audits like this
&lt;/h2&gt;

&lt;p&gt;I will not quote results from specific clients. What I can describe is the kind of thing that shows up again and again on sites like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The hero image is the LCP element and is lazy-loaded or missing a priority hint.&lt;/strong&gt; Cheap to fix, often the single biggest LCP gain. See &lt;a href="https://nayankyada.com/blog/how-to-fix-lcp-on-image-heavy-pages-nextjs" rel="noopener noreferrer"&gt;fixing LCP on image-heavy pages&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every page fetches whole documents.&lt;/strong&gt; Queries return full Portable Text bodies and every field on referenced documents when the template needs a title and a slug. The payload grows with the content, so the site gets slower as editors do their job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A client component wraps the whole page.&lt;/strong&gt; One top-level use client directive pulls a large tree into the JavaScript bundle. Splitting it is usually an afternoon, and the &lt;a href="https://nayankyada.com/blog/how-i-cut-nextjs-bundle-size-by-analysing-and-reducing-chunk-bloat" rel="noopener noreferrer"&gt;bundle size analysis post&lt;/a&gt; shows how to find it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revalidation is all or nothing.&lt;/strong&gt; Either nothing refreshes until the next deploy, or every publish invalidates the entire site. Tag-based revalidation from a Sanity webhook fixes both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Studio has document types nobody uses.&lt;/strong&gt; Old experiments sit in the sidebar and confuse new editors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data is present but wrong.&lt;/strong&gt; JSON-LD that references a logo URL that now returns 404, or an Article type on pages that are not articles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects live in three places.&lt;/strong&gt; Some in the CMS, some in config, some on the host, with chains between them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic. That is the point: most audit value comes from finding ordinary problems quickly and putting them in the right order.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the ranked fix list works
&lt;/h2&gt;

&lt;p&gt;The deliverable that matters is the ranked list. Each row should have four things: what is wrong, why it matters to the business, roughly how long the fix takes, and how we will verify it worked. Here is the shape I use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Verify with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hero image not prioritised on landing templates&lt;/td&gt;
&lt;td&gt;High (LCP)&lt;/td&gt;
&lt;td&gt;Under 2 hours&lt;/td&gt;
&lt;td&gt;Field LCP trend over 28 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Home page query returns full documents&lt;/td&gt;
&lt;td&gt;High (TTFB, payload)&lt;/td&gt;
&lt;td&gt;Half a day&lt;/td&gt;
&lt;td&gt;Response size and server timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Top-level client wrapper on article pages&lt;/td&gt;
&lt;td&gt;Medium (INP, bundle)&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;Bundle analyzer, INP trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Webhook revalidates entire site on every publish&lt;/td&gt;
&lt;td&gt;Medium (cost, stale pages)&lt;/td&gt;
&lt;td&gt;Half a day&lt;/td&gt;
&lt;td&gt;Cache hit rate after publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Unused document types in Studio&lt;/td&gt;
&lt;td&gt;Low (editor time)&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;td&gt;Editor walkthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ranking is by impact divided by effort, not by how alarming a Lighthouse score looks. A red metric that needs a two-week rebuild goes below a yellow one that needs an hour.&lt;/p&gt;

&lt;p&gt;You can use the list in three ways. Hand the whole thing to your in-house developer and have them work top to bottom. Hire me to fix the top five. Or take it to another developer for a competing quote, which is a perfectly reasonable thing to do. A good audit is portable; if you cannot use it without the person who wrote it, it was a sales funnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to judge whether the audit was worth it
&lt;/h2&gt;

&lt;p&gt;After the call, ask yourself these questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could someone else act on it?&lt;/strong&gt; If a competent developer can open the document and start work without a meeting, it passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did it say what not to fix?&lt;/strong&gt; Honest audits include a short list of things that look bad in a tool but do not matter for your traffic or goals. If every finding is urgent, nothing was prioritised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did it tie to money or rankings?&lt;/strong&gt; A fix for INP on a page with no organic traffic is lower priority than a fix for LCP on your best landing page. The &lt;a href="https://nayankyada.com/blog/marketing-website-performance-what-fast-pages-are-actually-worth" rel="noopener noreferrer"&gt;post on what fast pages are worth&lt;/a&gt; helps frame this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Were the claims checkable?&lt;/strong&gt; Each finding should name the page, the file or query, and the measurement. Vague statements like "improve image optimisation" are not findings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did you learn something about your own site?&lt;/strong&gt; Even if you fix nothing, you should now know where the time goes and which parts of the codebase are risky.&lt;/p&gt;

&lt;p&gt;Red flags: the report is mostly screenshots of automated tools, the author cannot explain a finding when asked, the audit recommends a full rebuild without ruling out cheaper fixes, or the price is tied to a retainer you must sign first.&lt;/p&gt;

&lt;p&gt;If the top fixes are small, the audit may pay for itself within a month through faster pages and fewer editor complaints. If the answer really is a larger change, such as a migration or an upgrade, you at least have a scoped number instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to prepare before you buy one
&lt;/h2&gt;

&lt;p&gt;Give the auditor read access to the repository, the Vercel project, the Sanity project, and Search Console. Decide in advance who will receive the findings and who can approve work. Write down your top three concerns in plain language, such as "our pricing page is slow on mobile" or "editors avoid creating case studies." A brief like that keeps the audit pointed at outcomes. If you are unsure how to frame it, the guide on &lt;a href="https://nayankyada.com/blog/how-to-write-a-project-brief-that-actually-helps-you-hire-a-nextjs-developer" rel="noopener noreferrer"&gt;writing a project brief for a Next.js developer&lt;/a&gt; applies here too.&lt;/p&gt;

&lt;p&gt;If your site is already visibly slow and you want to diagnose it yourself first, start with &lt;a href="https://nayankyada.com/blog/sanity-website-slow-how-to-find-the-cause-and-what-a-fix-costs" rel="noopener noreferrer"&gt;why a Sanity website is slow and what a fix costs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work I do
&lt;/h2&gt;

&lt;p&gt;A performance and Sanity audit with a ranked fix list is a fixed-price package I offer, starting from $800. Details are on the &lt;a href="https://dev.to/services#packages"&gt;services page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>corewebvitals</category>
      <category>sanitycms</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Is Sanity CMS right for my business? An honest decision guide</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 29 Oct 2026 10:59:52 +0000</pubDate>
      <link>https://dev.to/nayankyada/is-sanity-cms-right-for-my-business-an-honest-decision-guide-4c6l</link>
      <guid>https://dev.to/nayankyada/is-sanity-cms-right-for-my-business-an-honest-decision-guide-4c6l</guid>
      <description>&lt;p&gt;Most businesses asking whether Sanity CMS is right for them are really asking whether a custom build is worth the money over WordPress or Webflow. Sometimes it is. Often it is not, and I would rather tell you that before you spend anything.&lt;/p&gt;

&lt;p&gt;This is a decision guide for non-technical buyers. No jargon, just the questions I would ask you on a first call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sanity actually is
&lt;/h2&gt;

&lt;p&gt;Sanity is a content database with an editing interface called Sanity Studio. It does not make your website. A developer builds the site separately, usually in Next.js, and the site pulls content from Sanity.&lt;/p&gt;

&lt;p&gt;That split is the whole point. It gives you freedom over design, speed, and where content appears. It also means you are paying for a build, not installing a theme. If you want the longer technical background, &lt;a href="https://nayankyada.com/blog/headless-cms-setup-when-it-makes-sense-and-when-it-doesnt" rel="noopener noreferrer"&gt;Headless CMS setup: when it makes sense and when it doesn't&lt;/a&gt; covers it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Sanity is a great fit
&lt;/h2&gt;

&lt;p&gt;Sanity earns its cost when at least two or three of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your content has real structure: products, case studies, team members, events, locations, authors, each with their own fields and relationships.&lt;/li&gt;
&lt;li&gt;The same content appears in more than one place, such as your website, a mobile app, a newsletter, or a search index.&lt;/li&gt;
&lt;li&gt;Several people edit, and you need roles, approvals, scheduled publishing, or review before anything goes live.&lt;/li&gt;
&lt;li&gt;You have more than one language or region, with content that differs between them.&lt;/li&gt;
&lt;li&gt;Page speed and search visibility directly affect revenue.&lt;/li&gt;
&lt;li&gt;You have outgrown a WordPress site that is slow, fragile, or buried in plugins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you recognise your business in that list, the next step is a scoped conversation, not a sales pitch. See &lt;a href="https://nayankyada.com/blog/sanity-cms-website-cost-in-2026-what-founders-actually-pay" rel="noopener noreferrer"&gt;Sanity CMS website cost in 2026&lt;/a&gt; for realistic budgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Sanity is overkill
&lt;/h2&gt;

&lt;p&gt;Here is the part most developers skip. Sanity is probably the wrong choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The site is a brochure of roughly 5 to 15 pages that changes a few times a year.&lt;/li&gt;
&lt;li&gt;One person, maybe two, edits it, and they want to move boxes around visually.&lt;/li&gt;
&lt;li&gt;You need to launch in two or three weeks on a small budget.&lt;/li&gt;
&lt;li&gt;You have no developer relationship and no plan to build one.&lt;/li&gt;
&lt;li&gt;Your needs are covered by a store, booking tool, or membership plugin that already exists in WordPress or Shopify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases a template-based tool wins on cost and speed. Paying for custom engineering to host a five-page site is not a good use of money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team size and content complexity thresholds
&lt;/h2&gt;

&lt;p&gt;These are rules of thumb from practice, not hard laws. Treat them as a starting point for your own judgement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Likely best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 editor, under 15 pages, few updates&lt;/td&gt;
&lt;td&gt;Webflow or WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 to 3 editors, blog plus marketing pages, visual layout control wanted&lt;/td&gt;
&lt;td&gt;Webflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 to 5 editors, 3 or more content types, SEO important&lt;/td&gt;
&lt;td&gt;Sanity or well-run WordPress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5+ editors, approvals, roles, multiple languages&lt;/td&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content reused in app, site, and other channels&lt;/td&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large existing WordPress site, slow and plugin-heavy&lt;/td&gt;
&lt;td&gt;Sanity migration worth scoping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Content complexity matters more than team size. A single editor managing 40 interlinked product pages across three languages benefits from Sanity more than five people maintaining a simple blog.&lt;/p&gt;

&lt;p&gt;A useful test: count your content types, meaning distinct kinds of things with their own fields. One or two, and a template tool is fine. Six or more, with references between them, and structured content starts to pay for itself. &lt;a href="https://nayankyada.com/blog/sanity-nextjs-website-case-study-a-nonprofit-with-9-content-types" rel="noopener noreferrer"&gt;A nonprofit case study with nine content types&lt;/a&gt; shows what that looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where WordPress or Webflow wins honestly
&lt;/h2&gt;

&lt;p&gt;WordPress has a huge plugin ecosystem, cheap hosting, and almost every freelancer knows it. Webflow lets non-developers design visually and publish quickly. Both have hosting, forms, and basic SEO built in, so you are not stitching pieces together.&lt;/p&gt;

&lt;p&gt;The trade-off is control. As needs grow, you fight plugins in WordPress or hit CMS item and plan limits in Webflow. For a direct look, read &lt;a href="https://nayankyada.com/blog/sanity-vs-webflow-for-nextjs-in-2026-an-honest-comparison" rel="noopener noreferrer"&gt;Sanity vs Webflow for Next.js&lt;/a&gt; and &lt;a href="https://nayankyada.com/blog/sanity-vs-wordpress-headless-cms-when-headless-actually-beats-traditional" rel="noopener noreferrer"&gt;Sanity vs WordPress headless: when headless wins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your site works, loads reasonably, and your team is happy editing it, staying put is a legitimate answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs you should expect
&lt;/h2&gt;

&lt;p&gt;The software is rarely the big number. Sanity has a free plan that covers many small projects, and paid plans charge per seat after that. Confirm current limits on Sanity's pricing page before budgeting; they change. The real cost is the build and the ongoing care.&lt;/p&gt;

&lt;p&gt;My fixed-price Sanity + Next.js site builds start from $4,000, and moving an existing site into Sanity starts from $3,000. A WordPress or Webflow template site can cost well under that. If your budget is below roughly $3,000 and the site is simple, I will usually tell you to use a template tool. Details on packages are at &lt;a href="https://dev.to/services#packages"&gt;/services#packages&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also budget for maintenance. &lt;a href="https://nayankyada.com/blog/nextjs-website-maintenance-cost-what-a-sanity-site-runs-per-month" rel="noopener noreferrer"&gt;What a Sanity site runs per month&lt;/a&gt; breaks that down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions to ask yourself before deciding
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How often does content change, and who changes it?&lt;/li&gt;
&lt;li&gt;Does the same content need to appear anywhere besides the website?&lt;/li&gt;
&lt;li&gt;What does one extra second of load time cost us in leads or sales?&lt;/li&gt;
&lt;li&gt;Do we have, or can we hire, a developer for changes after launch?&lt;/li&gt;
&lt;li&gt;Are we solving a real limitation, or just a dated design?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last question matters. A redesign alone does not need a new CMS. If the only complaint is that the site looks old, a new theme might fix it for far less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags from the other side
&lt;/h2&gt;

&lt;p&gt;Be cautious of any developer who recommends Sanity without asking about your content, team, or budget. The same applies to anyone who calls it the answer for everything. A good recommendation comes from your constraints, not their preferred stack. If you are vetting someone, &lt;a href="https://nayankyada.com/blog/sanity-cms-development-agency-vs-freelancer-how-to-choose" rel="noopener noreferrer"&gt;Sanity CMS development agency vs freelancer&lt;/a&gt; is a useful companion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Choose Sanity if your content is structured, shared across channels, edited by a real team, or tied to performance that affects revenue. Choose WordPress or Webflow if your site is small, simple, and needs to launch cheaply. Stay where you are if nothing is actually broken.&lt;/p&gt;

&lt;p&gt;If you are on the fence, a short conversation costs nothing, and I will tell you straight if a template tool is the better call. If you decide Sanity fits, you can &lt;a href="https://dev.to/hire-sanity-developer"&gt;hire me as a Sanity developer&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Sanity Next.js website case study: a nonprofit with 9 content types</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:42:55 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-nextjs-website-case-study-a-nonprofit-with-9-content-types-3g0p</link>
      <guid>https://dev.to/nayankyada/sanity-nextjs-website-case-study-a-nonprofit-with-9-content-types-3g0p</guid>
      <description>&lt;p&gt;This is a composite-style account of a build I do often: a nonprofit organisation with a lot of different kinds of content, rebuilt on Sanity and Next.js. The client stays unnamed, and I have left out numbers I cannot stand behind. What follows is the scope, the decisions that mattered, and what a buyer should expect from the process in a Sanity Next.js website case study like this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The situation we started with
&lt;/h2&gt;

&lt;p&gt;The organisation had an ageing site that a volunteer had built years earlier. Staff dreaded updating it. Events were copied by hand from a spreadsheet, team bios lived in three places, and every annual report was a PDF with no web version. Nobody could say which pages were still accurate.&lt;/p&gt;

&lt;p&gt;The brief was not "make it pretty". It was: let a four-person communications team publish without asking a developer, make the content reusable, and keep the site quick on weak mobile connections, because many of their supporters browse on phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope: nine content types, one rule
&lt;/h2&gt;

&lt;p&gt;Before any code, we listed every kind of thing the organisation publishes. The list came to nine:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content type&lt;/th&gt;
&lt;th&gt;Who edits it&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Programme&lt;/td&gt;
&lt;td&gt;Programmes lead&lt;/td&gt;
&lt;td&gt;Programme pages, homepage, funding pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;Comms team&lt;/td&gt;
&lt;td&gt;Events list, programme pages, homepage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News post&lt;/td&gt;
&lt;td&gt;Comms team&lt;/td&gt;
&lt;td&gt;News, homepage, newsletter export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team member&lt;/td&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;About, programme pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual report&lt;/td&gt;
&lt;td&gt;Director&lt;/td&gt;
&lt;td&gt;Reports archive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource (guide, toolkit)&lt;/td&gt;
&lt;td&gt;Programmes lead&lt;/td&gt;
&lt;td&gt;Resource library, programme pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partner&lt;/td&gt;
&lt;td&gt;Fundraising&lt;/td&gt;
&lt;td&gt;Partners page, footer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Story (beneficiary or supporter)&lt;/td&gt;
&lt;td&gt;Comms team&lt;/td&gt;
&lt;td&gt;Homepage, programme pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic page&lt;/td&gt;
&lt;td&gt;Anyone&lt;/td&gt;
&lt;td&gt;About, policies, contact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one rule I set: if a fact appears in two places, it is stored once and referenced. A team member is a document, not text pasted into a page. That rule drove nearly every schema decision. If you are scoping something similar, my notes on &lt;a href="https://nayankyada.com/blog/nextjs-project-scope-what-to-define-before-you-write-a-line-of-code" rel="noopener noreferrer"&gt;what to define before you write a line of code&lt;/a&gt; cover the same discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema decisions that paid off
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;References over copies.&lt;/strong&gt; Programmes reference team members, events, resources and stories. An editor updating a staff title changes it once, and every page reflects it. The cost is query design, which is why I shape schemas with the rendering in mind, as described in &lt;a href="https://nayankyada.com/blog/how-i-structure-sanity-schemas-to-avoid-query-waterfalls-in-nextjs" rel="noopener noreferrer"&gt;structuring Sanity schemas to avoid query waterfalls&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small set of page-builder blocks.&lt;/strong&gt; The generic page type has a limited list of blocks: rich text, image with caption, call to action, callout, and a card grid. I resisted a thirty-block library. Every extra block is something an editor has to learn and a developer has to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events with real dates.&lt;/strong&gt; Events use proper datetime fields plus a validation rule that the end cannot precede the start. The site then filters upcoming and past events automatically, with no one archiving anything by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annual reports as structured content.&lt;/strong&gt; Instead of linking to a PDF alone, each report has a summary, key figures, sections in Portable Text, and the PDF as a download. That gave the organisation a searchable, linkable web version, which also helps search visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation as training.&lt;/strong&gt; Required alt text on images, a character limit on meta descriptions, and slug checks for uniqueness. Rules like these teach editors the right habits without a manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next.js side
&lt;/h2&gt;

&lt;p&gt;The front end uses Next.js App Router with server components, so most pages ship little JavaScript. Content pages are cached and refreshed when an editor publishes, using Sanity webhooks to trigger revalidation. Editors see the update within moments, and visitors get cached pages. Draft preview lets the comms team check a story before it goes live.&lt;/p&gt;

&lt;p&gt;For a content-heavy nonprofit, I treat performance as a requirement and not a bonus. Images come from Sanity with hotspot data so crops stay sensible, and layout dimensions are set from image metadata so pages do not jump while loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editor training
&lt;/h2&gt;

&lt;p&gt;This is the part most case studies skip, and it decides whether the project works a year later.&lt;/p&gt;

&lt;p&gt;We ran one live session of about ninety minutes with the whole communications team, using their real content, not demo text. The structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A tour of the Studio sidebar, which shows only the nine types in the order the team thinks about them.&lt;/li&gt;
&lt;li&gt;Publishing a news post from blank to live, including preview.&lt;/li&gt;
&lt;li&gt;Creating an event and watching it appear on the programme page it belongs to.&lt;/li&gt;
&lt;li&gt;What happens when you make a mistake: version history, unpublish, and who to ask.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They left with a one-page cheat sheet and a short recorded walkthrough. Two weeks later we held a thirty-minute follow-up to answer the questions that only appear once people use the tool. If you want the principles behind this, see &lt;a href="https://nayankyada.com/blog/cms-for-marketing-teams-what-to-set-up-before-editors-touch-it" rel="noopener noreferrer"&gt;what to set up before editors touch the CMS&lt;/a&gt; and &lt;a href="https://nayankyada.com/blog/sanity-studio-editor-experience-why-editors-avoid-it-and-the-fix" rel="noopener noreferrer"&gt;why editors avoid the Studio and the fix&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch
&lt;/h2&gt;

&lt;p&gt;Launch was boring on purpose. We built a redirect map from every old URL before cutover, tested it on a staging address the client could open, and checked that titles, descriptions and canonical tags were present on the new pages. Sitemaps and robots rules were verified, and the old domain's top pages were spot-checked after go-live. The approach follows the same checklist as any &lt;a href="https://nayankyada.com/blog/website-redesign-without-losing-seo-a-sanity-nextjs-checklist" rel="noopener noreferrer"&gt;redesign that protects search rankings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The client approved content on staging for a week before launch, which is when most corrections surface. Hold that review window. Compressing it is how typos reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results, and what I will not claim
&lt;/h2&gt;

&lt;p&gt;I am not going to quote traffic percentages or conversion lifts here, because those depend on the organisation, its starting point and seasonality, and inventing them would be dishonest. What I can describe are the outcomes that were observable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Staff publish news and events without developer involvement.&lt;/li&gt;
&lt;li&gt;Team and programme details are edited once, not in several places.&lt;/li&gt;
&lt;li&gt;Pages pass Core Web Vitals checks on mobile in field data once enough visits accumulate.&lt;/li&gt;
&lt;li&gt;Annual reports exist as web pages, not just downloads.&lt;/li&gt;
&lt;li&gt;The organisation owns its content in a structured form, so a future redesign does not mean re-keying everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a buyer should take from this
&lt;/h2&gt;

&lt;p&gt;Three questions would have saved this organisation years of frustration, and they apply to any similar project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can the developer list your content types and relationships before proposing a design?&lt;/li&gt;
&lt;li&gt;Will your actual editors be trained on your actual content?&lt;/li&gt;
&lt;li&gt;At handover, do you receive the code, the Studio, the documentation and the accounts in your name? There is a full list in the &lt;a href="https://nayankyada.com/blog/sanity-cms-project-handover-checklist-what-you-should-receive" rel="noopener noreferrer"&gt;handover checklist&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A project like this typically sits within my fixed-price Sanity + Next.js site build, which starts from $4,000, with scope driving the final number. Details are on the &lt;a href="https://dev.to/services#packages"&gt;services page&lt;/a&gt;. If you are weighing a similar rebuild, the &lt;a href="https://dev.to/hire-sanity-developer"&gt;hire a Sanity developer&lt;/a&gt; page explains how I work and how to start.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>nextjs</category>
      <category>headlesscms</category>
      <category>sanitystudio</category>
    </item>
    <item>
      <title>Fix Core Web Vitals on a Next.js + Sanity Site: Fix Order and Timeline</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:56:10 +0000</pubDate>
      <link>https://dev.to/nayankyada/fix-core-web-vitals-on-a-nextjs-sanity-site-fix-order-and-timeline-2l83</link>
      <guid>https://dev.to/nayankyada/fix-core-web-vitals-on-a-nextjs-sanity-site-fix-order-and-timeline-2l83</guid>
      <description>&lt;p&gt;Your Search Console report says "Poor URLs" or "Needs improvement" and your Next.js + Sanity site is supposed to be fast. This post is for owners who need to fix Core Web Vitals on a Next.js Sanity site and want to know which metric is failing, what is usually behind it, what order to fix things in, and how long until the report goes green.&lt;/p&gt;

&lt;p&gt;If you want the deeper theory, I cover thresholds in &lt;a href="https://nayankyada.com/blog/importance-of-core-web-vitals" rel="noopener noreferrer"&gt;Core Web Vitals in 2026&lt;/a&gt;. This post is the triage version: what to do on Monday morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the Search Console report correctly
&lt;/h2&gt;

&lt;p&gt;Three things trip owners up before any code is touched.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Field data, not lab data.&lt;/strong&gt; Search Console shows what real Chrome users experienced over a rolling 28-day window. A green Lighthouse score does not override it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL groups.&lt;/strong&gt; Google groups similar pages. If your 400 blog posts share a template, one bad template fails all 400. That is good news: one fix clears a whole group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile first.&lt;/strong&gt; Mobile and desktop are reported separately. Mobile usually fails first, so start there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thresholds at the 75th percentile are: LCP at or under 2.5 seconds, INP at or under 200 milliseconds, CLS at or under 0.1. Open the failing group, note which metric is flagged, and ignore the other two until that one is dealt with.&lt;/p&gt;

&lt;h2&gt;
  
  
  LCP failing: the hero image is almost always the cause
&lt;/h2&gt;

&lt;p&gt;On Sanity sites, the Largest Contentful Paint element is nearly always a hero image or a large heading above a hero. The usual causes, in the order I check them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hero image is lazy-loaded. &lt;code&gt;next/image&lt;/code&gt; lazy-loads by default, so an above-the-fold image needs the &lt;code&gt;priority&lt;/code&gt; prop (or &lt;code&gt;loading="eager"&lt;/code&gt; with &lt;code&gt;fetchPriority="high"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The image is served at original size because &lt;code&gt;sizes&lt;/code&gt; is missing or wrong, so a phone downloads a desktop-sized file.&lt;/li&gt;
&lt;li&gt;The page itself is slow to respond, which is a TTFB problem rather than an image problem.&lt;/li&gt;
&lt;li&gt;The image URL goes to Sanity's CDN with no width or format hints, so you ship a huge JPEG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A broken hero looks like this:&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/components/Hero.tsx (before)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&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/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;src&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="nl"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the fixed version:&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/components/Hero.tsx (after)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&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/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;src&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="nl"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nc"&gt;Image&lt;/span&gt;
      &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;
      &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-auto w-full"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the LCP is still slow after that, the problem is upstream. I walk through the full pattern in &lt;a href="https://nayankyada.com/blog/how-to-fix-lcp-on-image-heavy-pages-nextjs" rel="noopener noreferrer"&gt;How to fix LCP on image-heavy pages&lt;/a&gt;, and server response time is covered in the &lt;a href="https://nayankyada.com/blog/how-i-ship-sub-200ms-ttfb-on-sanity-powered-pages-with-ppr-and-edge" rel="noopener noreferrer"&gt;TTFB post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLS failing: images, fonts and late-loading blocks
&lt;/h2&gt;

&lt;p&gt;Layout shift on a Sanity site comes from a short list of suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images rendered without reserved space. Sanity returns width and height inside the asset metadata, so there is no excuse to omit them. See &lt;a href="https://nayankyada.com/blog/how-i-derive-sizes-width-and-height-from-sanity-metadata-to-kill-cls" rel="noopener noreferrer"&gt;Kill CLS on Sanity images&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Web fonts swapping in with different metrics. Use &lt;code&gt;next/font&lt;/code&gt;, which self-hosts the font and adjusts fallback metrics, rather than a stylesheet link to a font provider.&lt;/li&gt;
&lt;li&gt;Embeds, cookie banners and promo bars injected after load, pushing content down.&lt;/li&gt;
&lt;li&gt;Animations that move layout properties instead of transforms. If you use Framer Motion, check &lt;a href="https://nayankyada.com/blog/how-i-use-framer-motion-with-nextjs-app-router-without-layout-shift" rel="noopener noreferrer"&gt;this write-up&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fast way to find the offender: in Chrome DevTools, open the Performance panel, record a page load, and look at the Layout Shifts track. Each shift names the element that moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  INP failing: client JavaScript is the culprit
&lt;/h2&gt;

&lt;p&gt;Interaction to Next Paint measures how long the page takes to respond after a tap or click. It fails when the main thread is busy. On Next.js sites that usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too much of the tree is marked &lt;code&gt;use client&lt;/code&gt;, so a lot of JavaScript hydrates on every page.&lt;/li&gt;
&lt;li&gt;Third-party scripts: chat widgets, tag managers, heatmaps, consent tools.&lt;/li&gt;
&lt;li&gt;Heavy components such as a Portable Text renderer, carousel or search UI shipped to every visitor.&lt;/li&gt;
&lt;li&gt;Click handlers doing synchronous work like filtering large lists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is to push work to the server and ship less. Move &lt;code&gt;use client&lt;/code&gt; down to the smallest interactive leaf, defer third-party scripts, and render rich text on the server. Two related posts: &lt;a href="https://nayankyada.com/blog/how-i-cut-nextjs-bundle-size-by-analysing-and-reducing-chunk-bloat" rel="noopener noreferrer"&gt;cutting bundle size by analysing chunks&lt;/a&gt; and &lt;a href="https://nayankyada.com/blog/inp-for-react-apps-profiling-and-fixing-long-tasks" rel="noopener noreferrer"&gt;INP for React apps&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix order I use
&lt;/h2&gt;

&lt;p&gt;Cost and impact differ, so I do things in this order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Typical effort&lt;/th&gt;
&lt;th&gt;Metric helped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Priority and sizes on the hero image&lt;/td&gt;
&lt;td&gt;Under an hour&lt;/td&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Width and height on every image, next/font&lt;/td&gt;
&lt;td&gt;Half a day&lt;/td&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Reserve space for banners and embeds&lt;/td&gt;
&lt;td&gt;Half a day&lt;/td&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Defer or remove third-party scripts&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;INP, LCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Server-render rich text, shrink client components&lt;/td&gt;
&lt;td&gt;1 to 3 days&lt;/td&gt;
&lt;td&gt;INP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Caching and TTFB work&lt;/td&gt;
&lt;td&gt;1 to 2 days&lt;/td&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The effort figures are rough ranges for a typical marketing site, not quotes. A site with a messy component tree takes longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected timeline to green
&lt;/h2&gt;

&lt;p&gt;The timeline is set by Google, not by your developer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Days 1 to 5:&lt;/strong&gt; fixes shipped to production. Check the lab results in PageSpeed Insights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 1 to 4:&lt;/strong&gt; field data slowly improves as the 28-day window fills with fast visits. Vercel Analytics or a similar real-user tool shows progress much sooner than Search Console; here is &lt;a href="https://nayankyada.com/blog/how-i-set-up-core-web-vitals-monitoring-with-vercel-analytics-and-nextjs" rel="noopener noreferrer"&gt;how I set that up&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4 onward:&lt;/strong&gt; click "Validate fix" in Search Console. Google then monitors the group and typically confirms or rejects over roughly another four weeks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So plan for six to eight weeks from deploy to a clean report. If a promised fix claims green in a week, that is not how the data works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Which metric fails, on which URL group, and what element is responsible?&lt;/li&gt;
&lt;li&gt;How will you measure real-user data, not just Lighthouse?&lt;/li&gt;
&lt;li&gt;Will you fix the shared template or patch individual pages?&lt;/li&gt;
&lt;li&gt;What third-party scripts are on the site, and who approved each?&lt;/li&gt;
&lt;li&gt;What is the plan if field data has not moved after four weeks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a developer to find the cause rather than guess, this is work I do. A &lt;a href="https://dev.to/services#packages"&gt;Performance &amp;amp; Sanity audit&lt;/a&gt; starts from $800 and gives you a ranked fix list tied to your own Search Console groups.&lt;/p&gt;

</description>
      <category>corewebvitals</category>
      <category>performance</category>
      <category>nextjs</category>
      <category>sanitycms</category>
    </item>
  </channel>
</rss>
