<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Nayan Kyada</title>
    <description>The latest articles on Forem by Nayan Kyada (@nayankyada).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2638501%2Fb0cc4a93-db40-4087-9ff8-b4c2debac8a1.jpg</url>
      <title>Forem: Nayan Kyada</title>
      <link>https://forem.com/nayankyada</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nayankyada"/>
    <language>en</language>
    <item>
      <title>How I serve WebP and AVIF from Sanity without double-encoding in Next.js</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Mon, 25 May 2026 08:45:03 +0000</pubDate>
      <link>https://forem.com/nayankyada/how-i-serve-webp-and-avif-from-sanity-without-double-encoding-in-nextjs-lne</link>
      <guid>https://forem.com/nayankyada/how-i-serve-webp-and-avif-from-sanity-without-double-encoding-in-nextjs-lne</guid>
      <description>&lt;p&gt;Sanity's image CDN and Next.js both offer automatic WebP and AVIF conversion. When you stack them naively — passing a Sanity URL through &lt;code&gt;next/image&lt;/code&gt; without thinking — you end up with double-encoding: Vercel's optimisation layer fetches an already-optimised AVIF from Sanity, re-encodes it, and caches a bloated result under a cache key you can't easily predict. I've debugged this on three production sites; here is the setup that avoids it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why double-encoding is a real problem
&lt;/h2&gt;

&lt;p&gt;Next.js Image optimisation works by fetching the &lt;code&gt;src&lt;/code&gt; URL you pass in, running it through Sharp (or the Vercel edge equivalent), and caching the result keyed on &lt;code&gt;src + width + quality&lt;/code&gt;. If that &lt;code&gt;src&lt;/code&gt; already points to an AVIF served by Sanity's CDN, Sharp receives a compressed AVIF as input. It decodes it, loses some quality to generation loss, re-encodes it, and stores the result. The cached file is often &lt;em&gt;larger&lt;/em&gt; than what Sanity would have served directly, and the &lt;code&gt;/api/image&lt;/code&gt; route adds a round-trip for every unique &lt;code&gt;src&lt;/code&gt; string.&lt;/p&gt;

&lt;p&gt;The practical symptoms are a slow LCP on first uncached load, a 2–4× cache storage increase on Vercel's image cache, and confusing Lighthouse scores where the "serve images in modern formats" audit fires even though your CDN is already doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Sanity's &lt;code&gt;auto=format&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sanity's image URL builder exposes an &lt;code&gt;auto('format')&lt;/code&gt; method. When you call it, the CDN sniffs the &lt;code&gt;Accept&lt;/code&gt; header on the incoming request and returns AVIF, WebP, or JPEG accordingly — no query-string change, same URL, content-negotiated response.&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.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;imageUrlBuilder&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/image-url&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-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;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;imageUrlBuilder&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;urlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SanityImageSource&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="kr"&gt;number&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;80&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;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quality&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auto&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="c1"&gt;// CDN negotiates AVIF/WebP/JPEG per Accept header&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;The URL produced looks like:&lt;br&gt;
&lt;code&gt;https://cdn.sanity.io/images/&amp;lt;projectId&amp;gt;/&amp;lt;dataset&amp;gt;/abc123.jpg?w=800&amp;amp;q=80&amp;amp;auto=format&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Notice the extension is still &lt;code&gt;.jpg&lt;/code&gt;. The format decision happens at the edge, not in the URL. That matters for cache keys, which I'll explain below.&lt;/p&gt;
&lt;h2&gt;
  
  
  When to bypass &lt;code&gt;next/image&lt;/code&gt; entirely
&lt;/h2&gt;

&lt;p&gt;If the image is above-the-fold and you have exact &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; from Sanity metadata, you can skip &lt;code&gt;next/image&lt;/code&gt; and render a plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with a &lt;code&gt;srcSet&lt;/code&gt; built from Sanity URLs. You lose lazy loading and the built-in blur placeholder, but you gain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero Vercel image optimisation costs&lt;/li&gt;
&lt;li&gt;Sanity CDN cache hits instead of Vercel cache hits (better global PoP distribution)&lt;/li&gt;
&lt;li&gt;A stable, predictable URL for &lt;code&gt;&amp;lt;link rel="preload"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/SanityCdnImage.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;urlFor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/sanity-image&lt;/span&gt;&lt;span class="dl"&gt;'&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;SanityImageSource&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/image-url/lib/types/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SanityImageSource&lt;/span&gt;
  &lt;span class="nx"&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="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;WIDTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1600&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;function&lt;/span&gt; &lt;span class="nf"&gt;SanityCdnImage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&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="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priority&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srcSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WIDTHS&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;urlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&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;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;w`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&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;srcSet&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;srcSet&lt;/span&gt;&lt;span class="si"&gt;}&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;"(max-width: 768px) 100vw, 800px"&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="nx"&gt;width&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="nx"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eager&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;lazy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;decoding&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"async"&lt;/span&gt;
      &lt;span class="na"&gt;fetchPriority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This works well for hero images where you know &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; at build time from Sanity's &lt;code&gt;asset.metadata.dimensions&lt;/code&gt;. For thumbnails in a grid where layout shifts are unlikely and you want lazy loading managed by the browser, this is all you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  When to keep &lt;code&gt;next/image&lt;/code&gt; in the pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; is worth keeping for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images served from a remote origin that isn't Sanity (user-uploaded avatars, partner logos)&lt;/li&gt;
&lt;li&gt;Cases where you want the automatic &lt;code&gt;blur&lt;/code&gt; placeholder without rolling your own LQIP&lt;/li&gt;
&lt;li&gt;Responsive images inside MDX or Portable Text where you don't control the render context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do use &lt;code&gt;next/image&lt;/code&gt; with a Sanity &lt;code&gt;src&lt;/code&gt;, configure the loader to strip &lt;code&gt;auto=format&lt;/code&gt; and let Next.js handle format negotiation instead of Sanity. Mixing both means the Accept header is set by Vercel's optimisation server, not the browser, so content negotiation still works — but Sanity will serve AVIF to Vercel's server regardless of the end user's browser support, which is fine but redundant.&lt;/p&gt;

&lt;p&gt;The cleaner approach is to set a custom loader that removes &lt;code&gt;auto=format&lt;/code&gt; and adds explicit format parameters based on the loader's context:&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-next-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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanityLoader&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="kr"&gt;string&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="c1"&gt;// Remove auto=format — next/image handles format negotiation&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="k"&gt;delete&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="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;80&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;p&gt;Pass it as &lt;code&gt;loader={sanityLoader}&lt;/code&gt; on any &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component that uses a Sanity CDN URL. Now the cache key Next.js builds is based on a plain JPEG URL, and Sharp receives the JPEG — not an AVIF — as input. Quality loss from re-encoding is minimal, and the output size is predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache key hygiene
&lt;/h2&gt;

&lt;p&gt;Sanity's CDN caches on the full URL including query parameters. &lt;code&gt;auto=format&lt;/code&gt; doesn't change the cache key because format selection is header-based at the Sanity edge — same URL, different response body per client. That means two users with different browser support hit the same Sanity URL and get format-appropriate responses without busting the cache.&lt;/p&gt;

&lt;p&gt;Vercel's image cache, on the other hand, keys on the &lt;code&gt;src&lt;/code&gt; string as passed to &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;. If you let &lt;code&gt;auto=format&lt;/code&gt; stay in the URL &lt;em&gt;and&lt;/em&gt; route through &lt;code&gt;next/image&lt;/code&gt;, Vercel caches one response per &lt;code&gt;src + width + quality&lt;/code&gt; combination, and that cached response is whatever format Vercel's server negotiated with Sanity (almost always AVIF). Users on older browsers that don't support AVIF will still receive the correct format because the browser never sees the Sanity URL directly — it sees the Vercel &lt;code&gt;/api/image&lt;/code&gt; URL. So it isn't broken, just wasteful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I follow on every project
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Above-the-fold images with known dimensions: plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; + Sanity CDN + &lt;code&gt;auto=format&lt;/code&gt;. Preload with &lt;code&gt;&amp;lt;link rel="preload" as="image" imagesrcset="..."&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Below-the-fold images in grids or listings: same, but no preload.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/image&lt;/code&gt; only when you need blur placeholders or are sourcing from a non-Sanity origin. Use the custom loader to strip &lt;code&gt;auto=format&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps Sanity's CDN doing the format work it's already optimised for, avoids re-encoding overhead on Vercel, and gives you a clear mental model for where each image's cache lives.&lt;/p&gt;

</description>
      <category>sanity</category>
      <category>nextimage</category>
      <category>imageoptimisation</category>
      <category>webp</category>
    </item>
    <item>
      <title>What a real Sanity CMS development services proposal looks like</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sun, 24 May 2026 07:36:15 +0000</pubDate>
      <link>https://forem.com/nayankyada/what-a-real-sanity-cms-development-services-proposal-looks-like-3ce0</link>
      <guid>https://forem.com/nayankyada/what-a-real-sanity-cms-development-services-proposal-looks-like-3ce0</guid>
      <description>&lt;p&gt;A Sanity CMS development services proposal should tell you exactly what you're buying, when you'll see it, and what happens when scope shifts. Most proposals I see from developers — including ones I competed against early in my career — skip at least two of those three. Here is what a real one looks like, pulled from a proposal I sent to a mid-sized e-commerce brand in early 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the proposal structure actually contains
&lt;/h2&gt;

&lt;p&gt;A well-structured proposal is not a price list. It is a project contract in plain language. Mine runs four sections: scope, milestones with deliverables, payment schedule, and change-order policy. Buyers should expect all four. If a developer sends you a single paragraph with a number at the bottom, that is not a proposal — it is a quote, and quotes do not protect either party.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt; names every content type, every page template, every integration. My 2026 proposal listed: 9 Sanity schema types (product, article, author, category, FAQ, navigation, settings, redirect, legal page), one Next.js App Router front-end, Vercel deployment with preview URLs, and no Algolia or e-commerce API work unless a change order was signed. That last sentence is the boundary. Buyers often gloss over the exclusion list; it is actually the most important paragraph in the document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestones&lt;/strong&gt; map to real deliverables, not vague phases. Here is the milestone table from that proposal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Calendar day&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;Project kick-off&lt;/td&gt;
&lt;td&gt;Staging URL live, schema skeleton deployed, Studio login sent&lt;/td&gt;
&lt;td&gt;Day 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Schema complete&lt;/td&gt;
&lt;td&gt;All 9 document types, content entry guide (PDF)&lt;/td&gt;
&lt;td&gt;Day 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Front-end alpha&lt;/td&gt;
&lt;td&gt;Every page template rendering from real Sanity data&lt;/td&gt;
&lt;td&gt;Day 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;QA and revisions&lt;/td&gt;
&lt;td&gt;Two rounds of change requests, Core Web Vitals report&lt;/td&gt;
&lt;td&gt;Day 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Production launch&lt;/td&gt;
&lt;td&gt;DNS cutover, redirects live, handoff call recorded&lt;/td&gt;
&lt;td&gt;Day 30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The staging URL on day one is non-negotiable for me. It costs almost nothing to spin up a Vercel preview deployment from a skeleton repo, and it gives the client a live URL they can bookmark, share with their team, and hold me accountable against. A developer who cannot give you a staging URL within 24 hours of kick-off either has no deployment workflow or has not started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment schedule and what a fair split looks like
&lt;/h2&gt;

&lt;p&gt;My standard split for a 30-day Sanity project: 40% on contract signing, 30% on milestone 3 (front-end alpha), 30% on production launch. Some developers ask for 50% upfront on smaller projects, which is reasonable. What is not reasonable is 100% upfront or 100% on completion — the first exposes you to a developer who disappears, the second exposes the developer to a client who stalls sign-off indefinitely.&lt;/p&gt;

&lt;p&gt;For projects above ₹5 lakh or $6,000 USD, I split into four payments rather than three, adding a milestone 2 payment. This keeps cash flow predictable on both sides and creates natural checkpoints where either party can raise concerns before too much work has accumulated.&lt;/p&gt;

&lt;h2&gt;
  
  
  How scope creep is handled in writing
&lt;/h2&gt;

&lt;p&gt;The change-order clause in my proposals reads something close to this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any work outside the scope defined in Section 1 — including new schema types, new page templates, third-party integrations, or design changes after milestone 3 — requires a signed change order before work begins. Change orders are priced at [day rate] and billed at the next milestone payment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That clause protects both of us. Clients get a predictable process rather than surprise invoices. I get a paper trail rather than a verbal request that mutates over three Slack messages. The most common scope creep I see on Sanity projects: a client asks to add a blog after schema was already finalised, or they introduce a product configurator that requires a new document type mid-build. Neither is unreasonable — they just need to be priced and scheduled explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags to watch for in a proposal you receive
&lt;/h2&gt;

&lt;p&gt;If you are evaluating proposals from multiple developers, here is what to look for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No staging URL commitment.&lt;/strong&gt; If the proposal mentions a staging environment but gives no date, the developer has not thought through their deployment setup. Ask them directly: when will I receive a staging URL?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestones tied to hours, not deliverables.&lt;/strong&gt; "Week 1: 20 hours of development" tells you nothing. Milestones should name something you can see — a URL, a document, a deployed schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vague revision policy.&lt;/strong&gt; "Revisions included" is not a policy. How many rounds? What counts as a revision versus a change order? My proposals specify two rounds of QA changes, each submitted as a single consolidated list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No exclusion list.&lt;/strong&gt; A proposal that does not say what is out of scope implicitly includes everything. That is how a ₹3 lakh project becomes a ₹6 lakh project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payment on client satisfaction.&lt;/strong&gt; Final payment tied to subjective satisfaction — rather than a defined deliverable like production deployment — is a recipe for stalled projects. The deliverable should be objective: DNS pointing to the new site, redirects returning 301s, Lighthouse scores above a stated threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should ask before signing
&lt;/h2&gt;

&lt;p&gt;Three questions worth asking any developer before you accept a proposal: Who owns the Sanity project dataset if we part ways? (You should.) What is the handoff process — do you get a recorded walkthrough and written Studio guide? Is there a retainer option for post-launch support, and what is the response-time commitment?&lt;/p&gt;

&lt;p&gt;Ownership of the dataset is particularly important with Sanity because all your content lives in Sanity's hosted infrastructure. The project ID and dataset should be registered under your organisation's Sanity account, not the developer's. This is a one-minute configuration decision that some developers skip for convenience. Do not let them.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>clientfacing</category>
      <category>freelance</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>WordPress to Sanity migration cost: does it actually pay back?</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sat, 23 May 2026 11:58:49 +0000</pubDate>
      <link>https://forem.com/nayankyada/wordpress-to-sanity-migration-cost-does-it-actually-pay-back-5155</link>
      <guid>https://forem.com/nayankyada/wordpress-to-sanity-migration-cost-does-it-actually-pay-back-5155</guid>
      <description>&lt;p&gt;The question I hear most often from founders and marketing directors isn't 'how does Sanity work?' — it's 'what will this actually cost, and will we ever see that money back?' That's the right question. WordPress to Sanity migration cost isn't just the development invoice; it's the full before-and-after picture across hosting, plugins, editorial time, and organic traffic. Let me walk through each piece honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a migration typically costs to build
&lt;/h2&gt;

&lt;p&gt;Most WordPress-to-Sanity projects I scope fall into one of two buckets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smaller content sites&lt;/strong&gt; — ten to thirty page types, a blog, no complex integrations — run between £6,000 and £14,000 for design-to-deployment work. That assumes the design system either exists already or is being simplified during the migration, and that the editorial team is small (one to five people).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mid-size marketing or product sites&lt;/strong&gt; — fifty-plus page types, multi-author workflows, localisation, custom landing-page tooling — land between £18,000 and £40,000. The wide range is real: it depends on how many legacy page layouts need rebuilding and how much content needs manual restructuring rather than automated export.&lt;/p&gt;

&lt;p&gt;Those numbers assume a freelance senior developer or a small specialist agency. A large agency will charge more; an offshore team with no Sanity production experience will charge less but often costs more in rework.&lt;/p&gt;

&lt;p&gt;The migration itself — exporting WordPress content, mapping it to new Sanity schemas, running import scripts, and validating everything — typically adds £1,500 to £4,000 on top of the build cost depending on post volume and how many custom fields the old site accumulated over the years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money comes back
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Plugin licence savings.&lt;/strong&gt; A typical WordPress site that's been running for three or more years carries between £800 and £3,000 per year in plugin licences: SEO tools, form builders, page builders, caching plugins, security scanners, backup services, and whatever the previous agency installed and never removed. Most of those jobs are handled differently in a Next.js + Sanity setup — SEO metadata lives in code, forms go through a dedicated service like a simple API route, caching is handled by the CDN and ISR. Realistically, expect to save £600 to £2,000 per year on licences alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hosting savings.&lt;/strong&gt; Managed WordPress hosting for a site doing 50,000 to 200,000 monthly visits runs £80 to £400 per month, often more if the previous team over-provisioned to handle traffic spikes. Vercel's Pro plan plus Sanity's Growth plan together sit closer to £60 to £130 per month for the same traffic profile. That's a saving of £600 to £3,000 per year, and it scales better — a traffic spike doesn't require an emergency upgrade call with a host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editorial efficiency.&lt;/strong&gt; This one is harder to put a number on but often drives the decision more than hosting costs. WordPress's Gutenberg editor carries years of accumulated complexity, and most content teams I talk to have worked around it rather than with it — duplicating pages instead of using templates, avoiding blocks that behave unpredictably, waiting for a developer every time a new content type is needed. Sanity Studio is purpose-built for structured content. Editors get document-level previews, real-time co-editing, and a schema that matches exactly what they publish — no more orphaned fields from a plugin installed in 2019. Teams I've worked with report saving two to four hours per week in editorial overhead per person. At a conservative £30 per hour, that's £3,000 to £6,000 per year for a two-person team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance and SEO uplift.&lt;/strong&gt; This is the variable that can dwarf everything else, or amount to nothing, depending on your current baseline. If your WordPress site scores in the 40s on Core Web Vitals and you're competing for commercial search terms, moving to a statically generated or edge-cached Next.js front end typically moves LCP from 4–6 seconds to under 1.5 seconds. Google's ranking signals respond to that. I've seen clients recover from a position-eight plateau to page-one positions within three months of launch — that's meaningful revenue that wouldn't show up in a simple cost comparison. If your site already scores well and ranks well, the SEO case is weaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rough payback calculation
&lt;/h2&gt;

&lt;p&gt;Taking the conservative end: £800 licence savings + £600 hosting savings + £3,000 editorial time savings = £4,400 per year. Against a £12,000 migration project, that's a 2.7-year payback before counting any SEO uplift. If editorial savings are at the higher end and performance improvements move the needle on organic traffic, payback falls inside eighteen months.&lt;/p&gt;

&lt;p&gt;Against a £30,000 project, the maths requires either a larger editorial team, real SEO gains, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the migration doesn't pay back
&lt;/h2&gt;

&lt;p&gt;I'll be direct about this because I turn down projects more often than founders expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-traffic informational sites&lt;/strong&gt; — under 5,000 monthly visits, no commercial intent — have no realistic path to SEO-driven ROI, and the licence and hosting savings rarely justify a £10,000+ build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy editorial teams&lt;/strong&gt; are underrated. If your team knows Gutenberg, has built a workflow around it, and isn't complaining about publishing friction, you're not fixing a real problem. Migrating will create a training and transition cost that erases short-term savings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No upcoming rebrand or redesign.&lt;/strong&gt; The cleanest migrations happen when a visual refresh is already planned. Migrating content without touching the design means you're paying for a backend swap the user never sees. The ROI case is much weaker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sites with heavily WordPress-specific functionality&lt;/strong&gt; — WooCommerce, membership plugins, LearnDash — carry significant rebuild cost. It's not impossible, but the project scope balloons and payback extends accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to decide
&lt;/h2&gt;

&lt;p&gt;Before agreeing to a scope or signing anything, ask for an audit. A good developer should be able to spend three to five hours reviewing your current plugin list, hosting invoice, editorial workflow pain points, and traffic data, then give you a rough payback estimate. If they can't or won't do that, they're not thinking about your business — they're thinking about billable hours.&lt;/p&gt;

&lt;p&gt;The migration only makes sense if the numbers work or the editorial team is genuinely blocked. When both are true, it's one of the highest-ROI technical investments a content business can make.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>wordpress</category>
      <category>migration</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Five questions to ask before hiring a Sanity CMS developer</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 22 May 2026 07:57:57 +0000</pubDate>
      <link>https://forem.com/nayankyada/five-questions-to-ask-before-hiring-a-sanity-cms-developer-55hm</link>
      <guid>https://forem.com/nayankyada/five-questions-to-ask-before-hiring-a-sanity-cms-developer-55hm</guid>
      <description>&lt;p&gt;The questions to ask before hiring a Sanity CMS developer are not the ones most founders think to ask. It is tempting to lead with portfolio links and hourly rates, but those tell you almost nothing about whether the person can actually manage a complex content structure, keep your editors happy, or hand the project off cleanly when the engagement ends.&lt;/p&gt;

&lt;p&gt;Here are five questions that reveal what you actually need to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 1: How do you approach schema design before writing any code?
&lt;/h2&gt;

&lt;p&gt;Sanity's real power is its flexible content modelling — but that flexibility cuts both ways. A developer who jumps straight into building document types without a planning phase will create a schema that mirrors whatever the design says today, rather than one that supports how your content actually grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good answer looks like:&lt;/strong&gt; They ask about your content governance first. Who creates content, who approves it, and how many content types do you realistically publish? They mention separating content from presentation — keeping a "page" document lean and pulling in shared components through references rather than embedding everything directly. They might talk about planning for reuse across pages or channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag:&lt;/strong&gt; They say they will model the schema directly from the Figma file, or they cannot explain what a reference field is versus an inline object. That approach tends to produce schemas that are hard to query, full of duplication, and painful to extend later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 2: Can you show me a GROQ query you wrote and explain why you structured it that way?
&lt;/h2&gt;

&lt;p&gt;GROQ is Sanity's query language. It is not difficult to learn at a surface level, but writing queries that are efficient, maintainable, and safe to expose in a production application takes real experience. This question is not a quiz — it is an invitation to hear how they think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good answer looks like:&lt;/strong&gt; They can walk through a real query from a past project, explain what the projection is doing, and describe a problem it solved — such as avoiding unnecessary data being sent to the browser, or resolving a reference in a single round trip. Bonus points if they mention GROQ projections, conditional fields, or query typing with Sanity TypeGen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag:&lt;/strong&gt; They say they just copy queries from the Sanity documentation and adjust them. That works for a simple blog, but it is a sign they have not worked through the performance and maintainability problems that come up on any non-trivial project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 3: Walk me through how a content change in Sanity gets to my live site
&lt;/h2&gt;

&lt;p&gt;This is the question most clients forget to ask, and it is the one that matters most for day-to-day publishing. The answer tells you whether the developer has actually shipped a production Sanity project or just built demos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good answer looks like:&lt;/strong&gt; They describe the full pipeline: an editor publishes in Sanity Studio, a webhook fires to the hosting platform (usually Vercel), and the relevant pages are revalidated so visitors see fresh content within seconds — not hours. They understand the difference between revalidating a single page and triggering a full rebuild, and they know why the former is almost always the right choice. They should also mention that webhooks need to be secured so random requests cannot trigger rebuilds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag:&lt;/strong&gt; They say the site will rebuild automatically, but they cannot explain how or how long it takes. A full rebuild on a large site can take five to ten minutes. If your editors publish breaking news or time-sensitive product updates, that is not acceptable, and a developer who does not flag it has probably not thought through your workflow at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 4: How do you train editors to use the CMS you build?
&lt;/h2&gt;

&lt;p&gt;Sanity Studio can be configured to be genuinely pleasant for non-technical editors — or it can be a wall of fields that nobody wants to use. The developer controls most of that experience, and a good one takes it seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good answer looks like:&lt;/strong&gt; They mention customising the Studio layout so editors see only the document types relevant to them. They talk about adding field descriptions and validation messages written in plain language, not developer shorthand. Ideally they offer a short handoff session — recorded or live — so editors are not left guessing. Some developers also provide a brief written guide tailored to your specific setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag:&lt;/strong&gt; They hand over login credentials and a link to the Sanity documentation. Generic documentation does not explain your custom fields, your publishing workflow, or your specific constraints. Editors who are confused will either publish incorrectly or stop using the CMS altogether, which defeats the purpose of the investment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 5: What does post-launch support look like with you?
&lt;/h2&gt;

&lt;p&gt;Every production CMS project needs some level of support after go-live — a schema change for a new content type, a query that needs adjusting for a new page layout, or a webhook that silently stops firing. How a developer answers this question tells you a lot about how professional they are to work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good answer looks like:&lt;/strong&gt; They offer a clearly scoped support period — typically 30 to 90 days — with defined response times and a description of what is included versus what would be a separate engagement. They are transparent about their availability and honest about what falls outside the original scope. They also document what they built: schema decisions, environment variables, webhook configurations, and deployment notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag:&lt;/strong&gt; They say to just message them on Slack if anything comes up. Informal support arrangements collapse when a developer moves to a new client, goes on holiday, or simply becomes harder to reach. You want a paper trail and a process, not a favour.&lt;/p&gt;




&lt;p&gt;Hiring the right Sanity developer is less about finding someone who knows the tool and more about finding someone who has shipped real projects, thought through the editor experience, and can explain their decisions clearly. These five questions get you there faster than any portfolio review.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>hiring</category>
      <category>clientfacing</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Why I Use Next.js + Sanity for Content Sites</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 17:02:56 +0000</pubDate>
      <link>https://forem.com/nayankyada/why-i-use-nextjs-sanity-for-content-sites-3dg8</link>
      <guid>https://forem.com/nayankyada/why-i-use-nextjs-sanity-for-content-sites-3dg8</guid>
      <description>&lt;p&gt;If you’re building a marketing site or content platform, you want three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pages that load fast,&lt;/li&gt;
&lt;li&gt;content that’s easy to edit,&lt;/li&gt;
&lt;li&gt;and an SEO setup you can trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most projects I ship, &lt;strong&gt;Next.js + Sanity&lt;/strong&gt; is the sweet spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Next.js gives you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Great performance defaults&lt;/strong&gt;: route-based code splitting, image optimisation, and server rendering when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata control&lt;/strong&gt;: canonicals, Open Graph, Twitter cards, and structured data can be treated as first-class code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment simplicity&lt;/strong&gt;: ship to Vercel (or any Node host) and keep it boring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When Next.js is the right tool (and when it’s not)
&lt;/h3&gt;

&lt;p&gt;Next.js is ideal when you care about &lt;strong&gt;speed + SEO + developer velocity&lt;/strong&gt; together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marketing sites&lt;/strong&gt; with landing pages that must load instantly and share well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content sites&lt;/strong&gt; where posts need to be crawlable, linkable, and structured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid apps&lt;/strong&gt; where some pages are static (blog) and others are dynamic (pricing, dashboards, gated content).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I &lt;em&gt;don’t&lt;/em&gt; reach for Next.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the site is purely static and will never need dynamic data, a simpler static generator can be enough.&lt;/li&gt;
&lt;li&gt;If you’re building an internal tool with no SEO needs, you may prioritise different trade-offs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The SEO primitives you get “as code”
&lt;/h3&gt;

&lt;p&gt;The big win is that SEO becomes part of your engineering surface area:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canonical URLs&lt;/strong&gt;: avoid duplicate indexing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenGraph/Twitter&lt;/strong&gt;: previews that look consistent across platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data&lt;/strong&gt; (JSON-LD): help Google understand the page type and relationships (author, breadcrumbs, collections).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemaps + robots&lt;/strong&gt;: generated + validated like any other build artifact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building a blog, that means every post can ship with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a canonical,&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;BlogPosting&lt;/code&gt; schema,&lt;/li&gt;
&lt;li&gt;and a stable OG image route (like &lt;code&gt;/api/og/blog/[slug]&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Sanity gives you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexible content modelling&lt;/strong&gt;: you can represent real business concepts instead of forcing everything into a “blog post” shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editorial velocity&lt;/strong&gt;: drafts, previews, and publishing without developer tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured SEO fields&lt;/strong&gt;: titles, descriptions, canonicals, and share images can be part of the schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sanity is not “just a CMS” — it’s a content database
&lt;/h3&gt;

&lt;p&gt;Most teams hit limits when their CMS only supports “Page” and “Post”.&lt;br&gt;
Sanity lets you model the real world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authors&lt;/strong&gt; (with bios, socials, headshots)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories&lt;/strong&gt; (and content verticals)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable blocks&lt;/strong&gt; (CTAs, testimonials, FAQs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships&lt;/strong&gt; (related posts, featured projects, “learn more” links)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure is what makes a site scale without becoming chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  A blog model that scales (simple but future-proof)
&lt;/h3&gt;

&lt;p&gt;If I’m setting up a blog, I start with a schema that supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;slug&lt;/strong&gt; (stable URL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;title + description&lt;/strong&gt; (SERP + social)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;publishedAt&lt;/strong&gt; (ordering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tags/categories&lt;/strong&gt; (internal navigation + topical authority)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;body&lt;/strong&gt; (portable rich text)&lt;/li&gt;
&lt;li&gt;optional &lt;strong&gt;featured image&lt;/strong&gt; (sharing + in-article media)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can keep it lightweight at first, and expand only when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sanity is another system to manage (datasets, roles, previews).&lt;/li&gt;
&lt;li&gt;If you only need a handful of posts, MDX in the repo can be enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What it costs (so you can plan properly)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More moving parts&lt;/strong&gt;: environment variables, datasets, permissions, preview URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More decisions up front&lt;/strong&gt;: your schema design affects how editors work every day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview complexity&lt;/strong&gt;: “draft vs published” needs a clean workflow (it’s worth it, but it’s work).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are deal-breakers — they’re just real.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to decide: MDX vs Sanity (quick framework)
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;MDX in the repo&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you’ll publish infrequently (or you’re the only editor),&lt;/li&gt;
&lt;li&gt;you want “blog as code” and don’t need editorial workflows,&lt;/li&gt;
&lt;li&gt;you care about shipping fast and keeping infra minimal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;strong&gt;Sanity&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple people need to publish,&lt;/li&gt;
&lt;li&gt;content types will grow beyond “blog post”,&lt;/li&gt;
&lt;li&gt;you want drafts, approvals, and previews,&lt;/li&gt;
&lt;li&gt;you want a long-term content pipeline (case studies, landing pages, docs).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A practical implementation plan (what I ship for clients)
&lt;/h2&gt;

&lt;p&gt;Here’s the sequence I follow for a high-performing content site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define content types&lt;/strong&gt;: start minimal (post, author, category).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build listing + detail pages&lt;/strong&gt;: &lt;code&gt;/blog&lt;/code&gt; and &lt;code&gt;/blog/[slug]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add technical SEO&lt;/strong&gt;: canonicals, JSON-LD, sitemap, RSS, OG images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add internal linking&lt;/strong&gt;: “related posts” + links from services/projects pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure + iterate&lt;/strong&gt;: Search Console, Core Web Vitals, and content refresh cycles.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes I see (and how to avoid them)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thin posts&lt;/strong&gt;: short posts without a unique angle won’t build authority. Prefer fewer, deeper articles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No internal links&lt;/strong&gt;: link your posts to relevant pages (and between posts) so crawlers understand structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstable slugs&lt;/strong&gt;: never change slugs once indexed unless you have redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing OG images&lt;/strong&gt;: social previews matter for distribution (and distribution matters for links).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>sanity</category>
      <category>seo</category>
    </item>
    <item>
      <title>Signs your WordPress site needs a headless CMS rebuild</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 17:00:52 +0000</pubDate>
      <link>https://forem.com/nayankyada/signs-your-wordpress-site-needs-a-headless-cms-rebuild-5agm</link>
      <guid>https://forem.com/nayankyada/signs-your-wordpress-site-needs-a-headless-cms-rebuild-5agm</guid>
      <description>&lt;p&gt;If your marketing team is filing support tickets just to change a headline, or your Google Ads campaigns are bleeding budget because the landing pages load in four seconds, your WordPress site may have hit a structural ceiling — not a content problem, not a design problem. A structural one. These are the signs your WordPress site needs a headless CMS rebuild, and knowing them early saves you from throwing more money at band-aids.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 'headless' actually means for your business
&lt;/h2&gt;

&lt;p&gt;WordPress bundles everything together: the place editors write content, the code that decides how it looks, and the server that delivers it to visitors. That bundle made setup fast in 2012. In 2026 it creates friction at every layer.&lt;/p&gt;

&lt;p&gt;A headless setup separates the content system from the presentation layer. Your editors still log in to a clean dashboard — usually something like Sanity Studio — and write, publish, and schedule exactly as before. But the front end is a purpose-built website (built on Next.js in my projects) that fetches that content and renders it at speed, with full control over layout, performance, and where the content goes next. The content isn't trapped in one theme's templates. It can feed your website, your mobile app, a digital signage screen in your showroom, or an email campaign — all from the same source.&lt;/p&gt;

&lt;p&gt;That separation is the point. It removes the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The warning signs worth taking seriously
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Editors are hacking around the theme.&lt;/strong&gt; I worked with a sports organisation whose content team had resorted to embedding raw HTML in text fields to get a two-column layout the theme didn't support. They'd built invisible spacer images to control padding. Every new page took 45 minutes and still looked slightly off. This is a sign that the editorial experience has calcified around a theme that was never designed for what the organisation actually needed to publish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance scores are killing paid-ad ROI.&lt;/strong&gt; A training academy I rebuilt was running Google Ads to a set of course landing pages. Their average mobile PageSpeed score was 41. Industry data is consistent here: pages loading beyond three seconds lose more than half of mobile visitors before the page even finishes loading. The academy's cost-per-conversion was two to three times what it should have been. The WordPress install had eleven active page-builder plugins, four of which loaded JavaScript on every page regardless of whether that page used them. No amount of caching plugin configuration was going to fix that. The payload was structural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev time is eaten by plugin conflicts.&lt;/strong&gt; When a WooCommerce update breaks the slider plugin which breaks the checkout page, and the fix involves downgrading a security patch — that's not bad luck, that's the compounding cost of a plugin ecosystem with no central contract. One housing developer I worked with was spending roughly two days per month on this kind of firefighting. That's 24 days a year of developer time producing zero new value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your content can't go anywhere except the website.&lt;/strong&gt; The housing developer also wanted to launch a buyer portal — a separate web app where prospective buyers could track a property's construction milestones. All the project descriptions, floor plans, and images lived in WordPress. Getting that content into the portal meant either duplicating it manually or building a fragile custom REST API around a database schema that was never designed to be queried that way. In a headless setup, content is structured from day one to be delivered anywhere via a clean API. The buyer portal becomes a straightforward project, not a six-month integration nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're adding pages by duplicating and editing old ones.&lt;/strong&gt; This is quieter than the others but just as telling. When there's no real component system — just a grab-bag of shortcodes and widget areas — teams default to cloning whatever page last worked. The result is a site with 200 pages that are all slightly different versions of three templates, none of them documented, and any design change has to be made 200 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rebuild project actually looks like
&lt;/h2&gt;

&lt;p&gt;A typical engagement runs eight to fourteen weeks depending on content volume and integration complexity. The first two weeks are spent on content modelling — mapping what you publish today into structured schemas that will work for years. This is the most important work and the part most agencies skip, which is why rebuilds sometimes recreate the same problems in a new tool.&lt;/p&gt;

&lt;p&gt;Editors get a staging Studio environment by week three or four. The goal is for them to feel confident before the old site is retired, not after. By the midpoint, the new site exists alongside the old one. You can compare performance, share it with stakeholders, and catch gaps without any public risk.&lt;/p&gt;

&lt;p&gt;Content migration from WordPress is usually scripted — posts, images, and structured data move across without manual copy-paste. The edge cases (custom post types built by a plugin that no longer exists, images stored in twelve different places) are where honest scoping matters. I flag these in discovery so there are no surprises at handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it's worth it — and when it isn't
&lt;/h2&gt;

&lt;p&gt;A headless rebuild earns its cost when performance is directly tied to revenue (paid advertising, e-commerce, lead generation), when content needs to reach more than one channel, or when editorial bottlenecks are slowing down a team that publishes frequently.&lt;/p&gt;

&lt;p&gt;It is not the right call for a five-page brochure site with no editorial team and no plans to grow. WordPress with a well-configured theme and no unnecessary plugins is genuinely fine for that use case. The rebuild conversation is for organisations that have outgrown the bundle — where the original setup is now fighting against what the business needs to do.&lt;/p&gt;

&lt;p&gt;If you recognise two or more of the situations above, the question isn't really whether to rebuild. It's whether to do it now or after the next plugin conflict takes down a campaign you can't afford to lose.&lt;/p&gt;

</description>
      <category>headlesscms</category>
      <category>wordpress</category>
      <category>sanitycms</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Sanity CMS vs Contentful for Next.js projects: an honest comparison</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 17:00:49 +0000</pubDate>
      <link>https://forem.com/nayankyada/sanity-cms-vs-contentful-for-nextjs-projects-an-honest-comparison-56l</link>
      <guid>https://forem.com/nayankyada/sanity-cms-vs-contentful-for-nextjs-projects-an-honest-comparison-56l</guid>
      <description>&lt;p&gt;When a client asks me to recommend a CMS for their Next.js project, the choice almost always comes down to Sanity CMS vs Contentful. Both are mature headless platforms with solid Next.js support, but they make very different bets on query language, pricing, and developer experience. I've shipped production projects on both, and the gap is more nuanced than the marketing suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually comparing
&lt;/h2&gt;

&lt;p&gt;Contentful is a SaaS-first platform. You get a hosted API, a structured content model editor, and a well-documented REST/GraphQL API. Everything is managed for you — schema migrations, Studio hosting, CDN delivery. That's the value proposition.&lt;/p&gt;

&lt;p&gt;Sanity ships a content lake (the hosted backend) plus an open-source Studio that you own, configure, and deploy yourself. Schemas live in your repo as TypeScript files. You query with GROQ, a purpose-built query language. The Studio runs as a Next.js route or a standalone Vite app depending on how you wire it.&lt;/p&gt;

&lt;p&gt;That distinction — schema-in-repo vs schema-in-dashboard — changes almost every downstream decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  GROQ vs GraphQL: the real query language trade-off
&lt;/h2&gt;

&lt;p&gt;This is where most developers form a strong opinion fast.&lt;/p&gt;

&lt;p&gt;Contentful's GraphQL API is standard and predictable. If your team already knows GraphQL, you're productive on day one. Tooling like GraphQL Code Generator gives you typed responses with minimal config.&lt;/p&gt;

&lt;p&gt;GROQ is Sanity's query language and, after a week with it, I find it meaningfully more expressive for content shapes. You can dereference, filter, slice, and project in a single query without nested fragment boilerplate. Here's a real GROQ query I use for a blog index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Used in: app/(blog)/blog/page.tsx
*[_type == "post" &amp;amp;&amp;amp; defined(publishedAt) &amp;amp;&amp;amp; !(_id in path("drafts.**"))]
  | order(publishedAt desc)[0...12] {
  _id,
  title,
  "slug": slug.current,
  publishedAt,
  "author": author-&amp;gt;{ name, "avatar": image.asset-&amp;gt;url },
  "categories": categories[]-&amp;gt;{ title, "slug": slug.current },
  "lqip": coverImage.asset-&amp;gt;metadata.lqip,
  "dimensions": coverImage.asset-&amp;gt;metadata.dimensions
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The equivalent in Contentful GraphQL requires separate &lt;code&gt;authorCollection&lt;/code&gt; and &lt;code&gt;categoryCollection&lt;/code&gt; queries or deeply nested fragments. It's not unmanageable, but it accumulates friction on complex content models.&lt;/p&gt;

&lt;p&gt;The downside for GROQ: it's proprietary. New engineers need to learn it, and there's no ecosystem of generic tooling. Sanity TypeGen partially bridges this by generating TypeScript types from your queries, but you have to run it as part of your build pipeline.&lt;/p&gt;

&lt;p&gt;Contentful GraphQL wins on ecosystem familiarity. GROQ wins on expressiveness for relational content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing for small teams in 2026
&lt;/h2&gt;

&lt;p&gt;This has shifted. As of mid-2026, Sanity's free tier gives you 3 users, 2 non-admin users, and a 500k API CDN request per month limit with 10 GB bandwidth. Their Growth plan starts around $15/month per seat.&lt;/p&gt;

&lt;p&gt;Contentful's free tier is more generous on seat count (5 users) but caps content types at 48 and API calls at 1M per month on the Community plan. Their Basic plan starts around $300/month flat, which jumps hard for small teams.&lt;/p&gt;

&lt;p&gt;For a freelance engagement with 2–4 editors and a modest content volume, Sanity is cheaper by a wide margin. For enterprise teams already on Contentful with existing contracts, switching cost outweighs the savings.&lt;/p&gt;

&lt;p&gt;The Sanity free tier is also genuinely usable for client handoffs — I regularly leave clients on the free plan for low-traffic sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js integration DX
&lt;/h2&gt;

&lt;p&gt;Both platforms have official Next.js packages. The experience differs in meaningful ways.&lt;/p&gt;

&lt;p&gt;Contentful's &lt;code&gt;contentful&lt;/code&gt; npm package is typed but ships its own SDK with a non-trivial bundle size. Fetching content in an RSC looks clean, but you lose granular control over caching because you're going through their SDK rather than native &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sanity's &lt;code&gt;next-sanity&lt;/code&gt; package wraps the Sanity client and plugs directly into Next.js's &lt;code&gt;fetch&lt;/code&gt; cache with &lt;code&gt;revalidate&lt;/code&gt; tags. This matters for ISR. Here's what a cache-tagged fetch looks like:&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)/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sanityFetch&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/fetch&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;postBySlugQuery&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/queries&lt;/span&gt;&lt;span class="dl"&gt;'&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;PostBySlugQueryResult&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/types&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;PostPage&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="nl"&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;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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sanityFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostBySlugQueryResult&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;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postBySlugQuery&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="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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostLayout&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&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;The &lt;code&gt;tags&lt;/code&gt; array maps directly to on-demand revalidation from a Sanity webhook. Contentful can do tag-based revalidation too, but it requires more manual wiring through their webhook payload.&lt;/p&gt;

&lt;p&gt;Sanity also wins on Portable Text. Contentful's Rich Text requires &lt;code&gt;@contentful/rich-text-react-renderer&lt;/code&gt;, which outputs relatively flat HTML. Sanity's Portable Text lets you map block types to custom React components — useful when editors need inline callouts, embedded components, or custom image crops inside prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Contentful wins
&lt;/h2&gt;

&lt;p&gt;Contentful's content model tooling in the web UI is more approachable for non-developers. A content strategist can add a field without touching code. For teams where the CMS owner is not a developer, that matters.&lt;/p&gt;

&lt;p&gt;Contentful also has stronger built-in localization support at the field level, a more mature roles and permissions system, and a larger library of third-party integrations (AI assistants, DAM connectors, translation workflows). If you're building for a mid-market brand with a dedicated content ops team, Contentful's workflow features justify the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Sanity wins
&lt;/h2&gt;

&lt;p&gt;Schema-in-repo is the right model for teams that treat content structure as part of the codebase. You get version control on your schema, environment-specific studios, and type safety from TypeGen. For a developer-led team, this is a significant productivity edge.&lt;/p&gt;

&lt;p&gt;The Studio is also genuinely customizable. Structure Builder, custom input components, and document badges let you shape the editorial experience for your specific content model — not the other way around.&lt;/p&gt;

&lt;p&gt;For Next.js specifically, the combination of GROQ's join capabilities, native &lt;code&gt;fetch&lt;/code&gt; cache integration, and Portable Text's component mapping makes Sanity the more productive platform for complex page architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I decide on a new project
&lt;/h2&gt;

&lt;p&gt;Three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who owns the content model long-term?&lt;/strong&gt; Developer-owned → Sanity. Marketing/content ops → Contentful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the team size and budget?&lt;/strong&gt; Under 5 editors on a lean budget → Sanity free tier covers it. Large team with compliance needs → Contentful's enterprise tier might already be the choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How complex is the content graph?&lt;/strong&gt; Many cross-references, portable text with custom blocks, image focal points → Sanity's tooling handles this more gracefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither platform is wrong. They serve different principals. But for Next.js projects built by a small developer-led team, Sanity's schema-in-code model and GROQ's query expressiveness usually win on the work that actually matters.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>contentful</category>
      <category>nextjs</category>
      <category>groq</category>
    </item>
    <item>
      <title>Sanity vs Strapi vs Payload CMS: an honest comparison for 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 17:00:20 +0000</pubDate>
      <link>https://forem.com/nayankyada/sanity-vs-strapi-vs-payload-cms-an-honest-comparison-for-2026-44li</link>
      <guid>https://forem.com/nayankyada/sanity-vs-strapi-vs-payload-cms-an-honest-comparison-for-2026-44li</guid>
      <description>&lt;p&gt;Choosing between Sanity, Strapi, and Payload is one of the questions I get most often from teams starting a greenfield Next.js project. All three are legitimate headless CMS options in 2026, but they solve meaningfully different problems. This post is a direct comparison across the dimensions that actually matter in production: pricing, developer experience, schema modelling, image handling, and how hard it is to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and hosting model
&lt;/h2&gt;

&lt;p&gt;This is the sharpest difference between the three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt; is fully managed SaaS. You pay per seat on the Growth plan (around $15/editor/month at the time of writing) once you exceed the free tier. The CDN, the Studio, the asset pipeline — all hosted by Sanity. There's no infrastructure to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strapi&lt;/strong&gt; is open-source and self-hosted by default. You run it on a VPS, Railway, Render, or your own Kubernetes cluster. The Community edition is free forever. Strapi Cloud exists and gives you a managed option, but most teams I've seen pick Strapi specifically &lt;em&gt;because&lt;/em&gt; they want control over where data lives — data-residency requirements, GDPR, or just cost certainty at scale. If you have 50 editors, Strapi won't invoice you $750/month for seats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt; is also open-source and ships as a Node package that runs inside your own project. There's no separate Strapi-style server — Payload &lt;em&gt;is&lt;/em&gt; your backend. It connects to MongoDB or Postgres (Postgres support matured significantly in v3) and exposes a REST and GraphQL API plus a generated Admin UI. Payload Cloud exists for managed hosting, but the local dev story requires zero external services.&lt;/p&gt;

&lt;p&gt;Winner on cost at scale: &lt;strong&gt;Strapi or Payload&lt;/strong&gt; — neither charges per-seat, and both can run on hardware you already own.&lt;/p&gt;

&lt;p&gt;Winner for teams that don't want to run servers: &lt;strong&gt;Sanity&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience and schema modelling
&lt;/h2&gt;

&lt;p&gt;All three define schemas in code, but the ergonomics differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity schemas&lt;/strong&gt; live in TypeScript files and feed directly into Sanity Studio. The type system is mature, TypeGen generates fully-typed GROQ query results, and the Studio renders your schema as a polished editing UI without extra configuration. The constraint is that Sanity's content lake is a proprietary document store — you don't own the database, and your data model is shaped by Sanity's document/field primitives.&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/schemas/article.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;defineType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineField&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineType&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;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&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;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&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;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strapi schemas&lt;/strong&gt; are defined either through a GUI in the Content-Type Builder or by editing JSON files in &lt;code&gt;src/api/&amp;lt;name&amp;gt;/content-types/&lt;/code&gt;. The GUI is beginner-friendly but can produce messy diffs in version control. Teams that commit to code-first schema editing in Strapi end up with a solid workflow, but it takes discipline to avoid drift between local and production schema state. Relations in Strapi map to actual SQL joins, which is useful when you need to run arbitrary Postgres queries alongside the CMS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload schemas&lt;/strong&gt; feel the most like writing a database ORM. You define collections in TypeScript and Payload generates the Admin UI, REST endpoints, and database migrations automatically. If your team already knows Drizzle or Prisma, Payload's schema syntax will feel familiar. The tight Postgres integration means you can join CMS data with application tables in the same database — a real advantage for product teams building SaaS or e-commerce where content and business data coexist.&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;// payload/collections/Articles.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;CollectionConfig&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;payload&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;Articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CollectionConfig&lt;/span&gt; &lt;span class="o"&gt;=&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&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;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;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&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="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;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;richText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;author&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relationship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;relationTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;useAsTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&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;Winner on DX for content-rich sites: &lt;strong&gt;Sanity&lt;/strong&gt; — TypeGen plus GROQ plus the Studio is a complete, opinionated stack.&lt;/p&gt;

&lt;p&gt;Winner for Postgres-native product apps: &lt;strong&gt;Payload&lt;/strong&gt; — you get a CMS and an application database in one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editor UX
&lt;/h2&gt;

&lt;p&gt;This is where Sanity has a durable lead. The Studio is the most polished editing interface of the three. Real-time collaboration, document presence, Portable Text with inline components, image hotspot editing, and a structure builder for custom navigation — all are production-grade and have been refined over years.&lt;/p&gt;

&lt;p&gt;Strapi's Admin UI is functional and non-technical editors can learn it quickly, but it feels like a form builder rather than a publishing tool. There's no equivalent to Portable Text; rich text relies on a Quill or Slate integration that varies by version.&lt;/p&gt;

&lt;p&gt;Payload's Admin UI is impressive given how recently it was rebuilt in v3, but it's still primarily developer-facing. For content-heavy teams where editors work daily, the gap with Sanity is real.&lt;/p&gt;

&lt;p&gt;Winner on editor UX: &lt;strong&gt;Sanity&lt;/strong&gt;, and it's not close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image pipeline
&lt;/h2&gt;

&lt;p&gt;Sanity's image CDN is one of the strongest arguments for the platform. Images are stored in the content lake and served via &lt;code&gt;cdn.sanity.io&lt;/code&gt; with on-the-fly transforms: width, height, format (WebP/AVIF), quality, crop, and hotspot-aware focal cropping. Combined with &lt;code&gt;next/image&lt;/code&gt; and a custom loader, you get automatic format negotiation and LCP-optimised delivery with minimal setup.&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;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;sanityLoader&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&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="s2"&gt;?w=&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="s2"&gt;&amp;amp;q=&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="s2"&gt;&amp;amp;auto=format&amp;amp;fit=crop`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strapi stores uploads locally or in an S3-compatible bucket via a provider plugin. There's no built-in image transform pipeline — you either run your own (Cloudinary plugin is common) or handle transforms at the Next.js layer. More moving parts, more configuration.&lt;/p&gt;

&lt;p&gt;Payload handles media similarly to Strapi: uploads go to disk or cloud storage, transforms require a plugin or an external service. The &lt;code&gt;@payloadcms/plugin-cloud-storage&lt;/code&gt; covers S3, GCS, and Azure, but image optimisation is still on you.&lt;/p&gt;

&lt;p&gt;Winner on image pipeline: &lt;strong&gt;Sanity&lt;/strong&gt; — the built-in CDN with on-the-fly transforms removes an entire category of infrastructure decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock-in and portability
&lt;/h2&gt;

&lt;p&gt;This is the honest conversation clients avoid until it's too late.&lt;/p&gt;

&lt;p&gt;Sanity stores content in a proprietary NDJSON document store. You can export all data via the API, and the format is readable, but your GROQ queries and schema primitives (especially Portable Text) don't map directly to any other CMS. Migrating away is a project, not an afternoon.&lt;/p&gt;

&lt;p&gt;Strapi and Payload both use standard SQL or MongoDB. Your data lives in tables or collections you own. Moving from Strapi to Payload (or to a raw Postgres app) is a SQL migration, not a CMS-to-CMS content export. That's a meaningful difference if you're building something long-lived and want optionality.&lt;/p&gt;

&lt;p&gt;Winner on portability: &lt;strong&gt;Strapi or Payload&lt;/strong&gt; — you own the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each one wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Sanity&lt;/strong&gt; when editor experience and image delivery are the priority — marketing sites, editorial platforms, content-heavy agencies. The managed CDN, Studio polish, and TypeGen workflow justify the seat cost for most content teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Strapi&lt;/strong&gt; when data residency, self-hosting, or seat-count economics are non-negotiable. Enterprise clients with GDPR requirements and 30+ editors will often mandate self-hosted; Strapi is the most mature option in that lane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Payload&lt;/strong&gt; when you're building a product app and want your CMS and application database in the same Postgres instance. Authentication, collections, and business data unified under one Node app, with a generated Admin UI included. It's the option that most blurs the line between CMS and application framework.&lt;/p&gt;

</description>
      <category>sanity</category>
      <category>strapi</category>
      <category>payloadcms</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Sanity CMS website cost in 2026: what founders actually pay</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 17:00:19 +0000</pubDate>
      <link>https://forem.com/nayankyada/sanity-cms-website-cost-in-2026-what-founders-actually-pay-2d3n</link>
      <guid>https://forem.com/nayankyada/sanity-cms-website-cost-in-2026-what-founders-actually-pay-2d3n</guid>
      <description>&lt;p&gt;Budgeting for a Sanity CMS website is harder than it should be. The platform itself is free to start, which makes early quotes feel reassuring — then the final invoice lands and founders wonder where the number came from. This post breaks down every real cost driver so you can scope a project honestly before you hire anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What drives sanity cms website cost more than anything else
&lt;/h2&gt;

&lt;p&gt;The licence fee is almost never the problem. Sanity's free tier covers most small sites comfortably (up to three users, generous API limits). Growth and custom plan pricing starts to matter around 10+ editors or high-traffic content APIs, but even then you are looking at a few hundred dollars a month at most — not the dominant line item.&lt;/p&gt;

&lt;p&gt;What actually drives cost is the work required to model your content, build the editing experience your team will use every day, and connect the site to everything else your business runs on. Let me walk through each layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content modelling&lt;/strong&gt; is the architectural work done before a single page is built. A developer has to define what a "product", a "case study", or a "press release" looks like as structured data — what fields it has, what relationships it holds, what validation rules prevent editors from publishing broken content. A simple marketing site might need five or six document types. A content platform with tags, authors, series, and gated posts might need twenty, each with their own rules. More document types means more hours, and mistakes here are expensive to fix later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page count and template variety&lt;/strong&gt; compound the modelling work. Twelve pages built from three templates costs far less than twelve pages each with a unique layout. Before you get a quote, list your pages and honestly count how many are genuinely different from each other. Agencies and freelancers price template variety, not raw page count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The editor experience&lt;/strong&gt; is underquoted and then complained about. Sanity Studio is highly customisable — you can build a clean, opinionated interface that guides your content team, or you can ship the default and watch editors call you weekly. A well-structured studio with filtered views, conditional fields, and sensible document ordering takes real time to build. Budget for it. It pays back in reduced support requests from your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrations are where budgets stretch
&lt;/h2&gt;

&lt;p&gt;Sanity stores your content. It does not handle payments, email, search, or video — and most real products need at least one of those.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt;: Connecting a product catalogue in Sanity to Stripe for checkout adds meaningful complexity. You need to decide what lives in Sanity (marketing copy, images, variant descriptions) versus what lives in Stripe (prices, inventory, webhooks). Scoping that boundary alone is a half-day conversation. Building it is typically two to four days of development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SendGrid&lt;/strong&gt;: Triggered emails off content events — a new post published, a form submitted, a membership renewed — require route handlers that listen for Sanity webhooks and call SendGrid's API. Straightforward in isolation, but each trigger adds test coverage and edge cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Algolia&lt;/strong&gt;: Full-text search across a large Sanity content library almost always lands on Algolia. You need a sync pipeline that pushes content to Algolia when it changes, an index schema that matches your search UX, and a search component on the front end. Expect three to five days for a well-tuned integration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mux&lt;/strong&gt;: Video-heavy sites — course platforms, media brands — use Mux for adaptive streaming. Uploading from Sanity Studio via a custom asset source, storing the Mux playback ID in your schema, and rendering a player with the right poster frame is around two to three days of work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each integration is a multiplier, not an add-on. If you want Stripe and Algolia and Mux, those do not add up linearly — shared infrastructure, authentication patterns, and error handling overlap in ways that skilled developers manage efficiently, but the work is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realistic cost ranges for three common project types
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Small marketing site&lt;/strong&gt; — five to eight pages, one blog section, one or two editors, no integrations beyond a contact form and basic analytics.&lt;/p&gt;

&lt;p&gt;Design-to-launch freelance rate: £4,000–£9,000 / $5,000–$12,000. Timeline: three to six weeks. Ongoing hosting: £20–£60/month (Vercel hobby or pro, Sanity free tier). This is the profile where Sanity's low entry cost genuinely shines. You get a clean editorial experience and a fast, modern front end at a sensible budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mid-size content platform&lt;/strong&gt; — blog with authors and tags, gated content, newsletter integration, Algolia search, twenty-plus document types, up to ten editors.&lt;/p&gt;

&lt;p&gt;Freelance or small agency rate: £18,000–£40,000 / $22,000–$50,000. Timeline: eight to sixteen weeks. Ongoing hosting: £100–£300/month including Sanity Growth plan, Vercel Pro, and Algolia's starter tier. The range is wide because content modelling complexity and design fidelity vary enormously at this tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex multi-locale build&lt;/strong&gt; — multiple languages with separate editorial workflows, market-specific pricing via Stripe, Mux video library, custom Sanity Studio plugins, five-plus integrations, fifteen-plus editors, compliance requirements.&lt;/p&gt;

&lt;p&gt;Agency rate: £70,000–£180,000 / $85,000–$220,000. Timeline: four to eight months. Ongoing hosting and tooling: £500–£2,000/month. Internationalisation alone — routing, translation workflows, locale-specific content fallbacks — adds weeks of development that most initial scopes underestimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editor training and ongoing maintenance
&lt;/h2&gt;

&lt;p&gt;Training is skipped in more proposals than I can count. A Sanity Studio that took six weeks to build still needs two to four hours of structured walkthrough for your editorial team, plus documentation written for non-technical staff. Budget £400–£1,200 for this. It prevents three months of avoidable support tickets.&lt;/p&gt;

&lt;p&gt;Maintenance is a separate question from hosting. Dependency updates, Sanity schema migrations when your content needs change, new page templates as your business grows — that work is either retained on a monthly contract (£400–£1,500/month is a common range for a freelance retainer) or quoted project by project. Neither is wrong, but know which model you are agreeing to before you sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get a quote you can actually trust
&lt;/h2&gt;

&lt;p&gt;Before approaching a developer or agency, write down: how many distinct page layouts you need, which third-party tools your site must talk to, how many people will edit content, and whether you need multiple languages. That list turns a vague conversation into a scopeable brief. Developers who quote from a brief are giving you a number they can defend. Developers who quote from a thirty-minute call are guessing — and you will pay for the gap.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>webdevelopmentcost</category>
      <category>clientguide</category>
    </item>
    <item>
      <title>INP for React Apps: Profiling and Eliminating Long Tasks</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 16:59:32 +0000</pubDate>
      <link>https://forem.com/nayankyada/inp-for-react-apps-profiling-and-eliminating-long-tasks-2ml1</link>
      <guid>https://forem.com/nayankyada/inp-for-react-apps-profiling-and-eliminating-long-tasks-2ml1</guid>
      <description>&lt;p&gt;INP (Interaction to Next Paint) measures &lt;strong&gt;how quickly your UI responds&lt;/strong&gt; after a user interacts.&lt;br&gt;
If a click, tap, or keypress is followed by a noticeable delay, you’ll feel it — and so will your users.&lt;/p&gt;

&lt;p&gt;INP is now the key responsiveness metric in Core Web Vitals, and it’s one of the most common issues on React apps that ship too much JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What INP actually measures (in plain terms)
&lt;/h2&gt;

&lt;p&gt;When a user interacts, the browser has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;run your event handler,&lt;/li&gt;
&lt;li&gt;run any state updates and rendering work,&lt;/li&gt;
&lt;li&gt;paint the next frame.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;INP captures the time from interaction to the next paint for &lt;strong&gt;the worst interactions&lt;/strong&gt; users experience (within a page view).&lt;/p&gt;

&lt;h3&gt;
  
  
  Targets (baseline)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Good: &lt;strong&gt;≤ 200ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Needs improvement: &lt;strong&gt;200–500ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Poor: &lt;strong&gt;&amp;gt; 500ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The main causes of bad INP in React apps
&lt;/h2&gt;

&lt;p&gt;In most apps, INP is bad because of one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long tasks&lt;/strong&gt; (main thread blocked for &amp;gt;50ms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render storms&lt;/strong&gt; (too many components re-rendering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy work inside event handlers&lt;/strong&gt; (sync parsing, sorting, filtering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party scripts&lt;/strong&gt; (analytics, chat widgets, tag managers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too much JS shipped&lt;/strong&gt; (hydration costs + runtime overhead)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t “optimize INP” by tweaking one thing — you reduce main-thread work and make updates cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Confirm you really have an INP problem
&lt;/h2&gt;

&lt;p&gt;Start with &lt;strong&gt;field data&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search Console’s Core Web Vitals report (pattern-level)&lt;/li&gt;
&lt;li&gt;RUM if you have it (best)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then use &lt;strong&gt;lab tools&lt;/strong&gt; to reproduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome DevTools Performance recording&lt;/li&gt;
&lt;li&gt;React DevTools Profiler&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Find long tasks (your #1 enemy)
&lt;/h2&gt;

&lt;p&gt;If the main thread is blocked, the browser can’t paint.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to spot them
&lt;/h3&gt;

&lt;p&gt;In a Performance recording:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look for long yellow blocks (scripting).&lt;/li&gt;
&lt;li&gt;Zoom into interactions and check what runs right after the input event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see repeated long tasks, you’ve found your INP root cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Make event handlers “light”
&lt;/h2&gt;

&lt;p&gt;Event handlers should ideally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;update state,&lt;/li&gt;
&lt;li&gt;schedule work,&lt;/li&gt;
&lt;li&gt;and return quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common anti-patterns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Doing expensive filtering/sorting synchronously on click&lt;/li&gt;
&lt;li&gt;Parsing large JSON payloads during input&lt;/li&gt;
&lt;li&gt;Building huge arrays/objects during a scroll/typing event&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix patterns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Precompute when possible (outside the interaction)&lt;/li&gt;
&lt;li&gt;Debounce expensive work triggered by typing&lt;/li&gt;
&lt;li&gt;Chunk big work into smaller pieces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Reduce React re-render costs
&lt;/h2&gt;

&lt;p&gt;Many INP problems are simply “too much renders happen per interaction”.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I check first
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are we passing new objects/functions every render?&lt;/li&gt;
&lt;li&gt;Are lists re-rendering on every keystroke?&lt;/li&gt;
&lt;li&gt;Is global state causing whole pages to update?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix patterns that consistently help
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memoize&lt;/strong&gt; hot components (only where it matters)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;stable props&lt;/strong&gt; (avoid &lt;code&gt;{}&lt;/code&gt; and &lt;code&gt;() =&amp;gt; {}&lt;/code&gt; inline for hot paths)&lt;/li&gt;
&lt;li&gt;Split state: keep “typing state” local, not global&lt;/li&gt;
&lt;li&gt;Virtualize big lists/grids&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn’t to “memo everything”. The goal is to stop re-rendering 200 components when the user clicks one button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Reduce hydration + client JS on content pages
&lt;/h2&gt;

&lt;p&gt;If your page is mostly content (blog posts), you usually don’t need much JS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best lever
&lt;/h3&gt;

&lt;p&gt;Avoid turning layout/typography into client components.&lt;/p&gt;

&lt;p&gt;Ship interaction only where needed (search box, filters, forms), and keep everything else server-rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Defer third-party scripts (they often dominate INP)
&lt;/h2&gt;

&lt;p&gt;Third-party scripts can easily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add long tasks,&lt;/li&gt;
&lt;li&gt;create layout thrash,&lt;/li&gt;
&lt;li&gt;or block the main thread during interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Practical strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Defer until after first interaction or idle&lt;/li&gt;
&lt;li&gt;Load only on routes that need it&lt;/li&gt;
&lt;li&gt;Remove anything you don’t use weekly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams keep scripts forever. INP improves fast when you treat scripts like dependencies with a cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Use a repeatable INP “playbook”
&lt;/h2&gt;

&lt;p&gt;This is my default workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify a bad interaction (field data or user report).&lt;/li&gt;
&lt;li&gt;Reproduce in DevTools.&lt;/li&gt;
&lt;li&gt;Find the longest task after the input event.&lt;/li&gt;
&lt;li&gt;Reduce work in the handler.&lt;/li&gt;
&lt;li&gt;Reduce re-renders triggered by that state update.&lt;/li&gt;
&lt;li&gt;Re-test and confirm the long task is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Event handlers return quickly&lt;/li&gt;
&lt;li&gt;Expensive work is deferred/chunked&lt;/li&gt;
&lt;li&gt;Large lists are virtualized&lt;/li&gt;
&lt;li&gt;Hot components are memoized appropriately&lt;/li&gt;
&lt;li&gt;Client JS is minimized on content routes&lt;/li&gt;
&lt;li&gt;Third-party scripts are deferred/audited&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>react</category>
      <category>corewebvitals</category>
    </item>
    <item>
      <title>Why Core Web Vitals Matter (and How I Improve Them)</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 16:59:31 +0000</pubDate>
      <link>https://forem.com/nayankyada/why-core-web-vitals-matter-and-how-i-improve-them-pj3</link>
      <guid>https://forem.com/nayankyada/why-core-web-vitals-matter-and-how-i-improve-them-pj3</guid>
      <description>&lt;p&gt;Core Web Vitals are Google’s &lt;strong&gt;real‑user performance signals&lt;/strong&gt; for how a page &lt;em&gt;feels&lt;/em&gt;.&lt;br&gt;
They’re not just “speed scores” — they influence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO&lt;/strong&gt; (page experience is a ranking signal),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;conversion rate&lt;/strong&gt; (slow, janky pages lose users),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;trust&lt;/strong&gt; (fast sites feel higher quality).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building a marketing site or content platform, improving Core Web Vitals is one of the highest-ROI technical tasks you can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Core Web Vitals?
&lt;/h2&gt;

&lt;p&gt;Today, the core metrics are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP (Largest Contentful Paint)&lt;/strong&gt;: how quickly the main content becomes visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP (Interaction to Next Paint)&lt;/strong&gt;: how responsive the page is to user input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLS (Cumulative Layout Shift)&lt;/strong&gt;: how stable the layout is while loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are measured using &lt;strong&gt;real user data&lt;/strong&gt; (field data), not only synthetic tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Target thresholds (what “good” means)
&lt;/h3&gt;

&lt;p&gt;Use these as your baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP&lt;/strong&gt;: good ≤ 2.5s, needs improvement 2.5–4.0s, poor &amp;gt; 4.0s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP&lt;/strong&gt;: good ≤ 200ms, needs improvement 200–500ms, poor &amp;gt; 500ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLS&lt;/strong&gt;: good ≤ 0.1, needs improvement 0.1–0.25, poor &amp;gt; 0.25&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hit “good” consistently, you’ll usually see improved crawl efficiency, higher engagement, and better rankings over time (especially in competitive queries where all content is similar).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why they matter for SEO (the practical view)
&lt;/h2&gt;

&lt;p&gt;Google has said for years: &lt;strong&gt;content relevance wins&lt;/strong&gt;. That’s still true.&lt;br&gt;
But on the margin — when two pages are equally relevant — performance can be the difference.&lt;/p&gt;

&lt;p&gt;Core Web Vitals matter most when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your niche is competitive (tooling, agencies, SaaS, ecommerce),&lt;/li&gt;
&lt;li&gt;users bounce quickly (landing pages, blog posts),&lt;/li&gt;
&lt;li&gt;your pages are media-heavy (images, embeds, third-party scripts).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also: good UX improves how users behave (time on site, browsing depth). Those aren’t direct ranking factors in a simple way, but they correlate strongly with sites that perform well in search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “field vs lab” trap
&lt;/h2&gt;

&lt;p&gt;Many teams ship optimisations that look great in Lighthouse but do nothing for real users.&lt;br&gt;
That’s because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lighthouse runs on a simulated device/network.&lt;/li&gt;
&lt;li&gt;Core Web Vitals in Search Console come from actual users across devices, geos, and connection types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;strong&gt;field data&lt;/strong&gt; to find the real problems.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;lab tools&lt;/strong&gt; to reproduce and fix them.&lt;/li&gt;
&lt;li&gt;Validate again in field data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to measure Core Web Vitals (what I use)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Field data (real users)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Search Console (Core Web Vitals report)&lt;/strong&gt;: broad view by template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrUX&lt;/strong&gt; (Chrome UX Report): site-level + page-level trends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RUM&lt;/strong&gt; (real-user monitoring): best if you want per-route and per-release tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lab data (debugging)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse&lt;/strong&gt;: quick checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools Performance&lt;/strong&gt;: deep INP/debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebPageTest&lt;/strong&gt;: waterfalls, filmstrip, CDN/cache behaviour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can’t measure reliably, you can’t improve reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  LCP: the fastest wins (and the common causes)
&lt;/h2&gt;

&lt;p&gt;LCP is usually dominated by one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a big hero image,&lt;/li&gt;
&lt;li&gt;a large heading block (custom fonts),&lt;/li&gt;
&lt;li&gt;a server response delay (TTFB),&lt;/li&gt;
&lt;li&gt;render-blocking CSS/JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fixes that consistently help LCP
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimise the LCP element&lt;/strong&gt;
Make the hero image properly sized, compressed, and served via CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload the right resources&lt;/strong&gt;
Fonts and the actual LCP image should load early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce TTFB&lt;/strong&gt;
Cache aggressively, avoid expensive server work on first request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid heavy client-side hydration&lt;/strong&gt;
Keep initial render simple; defer non-critical scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Next.js, the biggest LCP improvements usually come from &lt;strong&gt;image strategy + caching + avoiding unnecessary client components&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  INP: responsiveness (the one many teams ignore)
&lt;/h2&gt;

&lt;p&gt;INP measures how quickly the UI updates after an interaction (click, tap, type).&lt;br&gt;
Bad INP usually comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long tasks on the main thread,&lt;/li&gt;
&lt;li&gt;expensive React re-renders,&lt;/li&gt;
&lt;li&gt;heavy third-party scripts,&lt;/li&gt;
&lt;li&gt;too much JS on the initial page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fixes that move INP
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduce bundle size&lt;/strong&gt;: ship less JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split work&lt;/strong&gt;: move expensive logic to the server or a worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memoize thoughtfully&lt;/strong&gt;: avoid re-render storms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defer third-party&lt;/strong&gt;: load analytics/chat widgets after interaction or idle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your site is content-heavy (blog), INP improvements often come from simply reducing client JS.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLS: layout stability (easy to fix, big UX win)
&lt;/h2&gt;

&lt;p&gt;CLS happens when elements move after rendering.&lt;br&gt;
The common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;images without dimensions,&lt;/li&gt;
&lt;li&gt;fonts swapping late (FOIT/FOUT),&lt;/li&gt;
&lt;li&gt;injected banners/popups,&lt;/li&gt;
&lt;li&gt;components that load content without reserved space.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fixes for CLS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Always reserve space for images/media.&lt;/li&gt;
&lt;li&gt;Avoid late-injecting UI above content.&lt;/li&gt;
&lt;li&gt;Use stable font loading strategies.&lt;/li&gt;
&lt;li&gt;Skeletons should match final layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLS is one of the easiest vitals to improve — and users notice immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple checklist I apply on every site
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;: correct size, modern formats, CDN, don’t ship huge assets to mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: CDN + edge where possible; avoid dynamic work on every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript budget&lt;/strong&gt;: keep initial JS minimal, defer non-critical scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts&lt;/strong&gt;: preload only what you need, limit weights, avoid blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party&lt;/strong&gt;: audit everything; most scripts are performance tax.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What results to expect
&lt;/h2&gt;

&lt;p&gt;When you improve Core Web Vitals, the most common outcomes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;better engagement (scroll depth, time on page),&lt;/li&gt;
&lt;li&gt;improved conversion rate on landing pages,&lt;/li&gt;
&lt;li&gt;more consistent rankings (especially where competitors are similar).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the best part: these gains compound. Every future page you publish benefits from the same performance foundation.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>performance</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Sanity vs WordPress headless CMS: when headless actually beats traditional</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 20 May 2026 16:58:09 +0000</pubDate>
      <link>https://forem.com/nayankyada/sanity-vs-wordpress-headless-cms-when-headless-actually-beats-traditional-3kc5</link>
      <guid>https://forem.com/nayankyada/sanity-vs-wordpress-headless-cms-when-headless-actually-beats-traditional-3kc5</guid>
      <description>&lt;p&gt;Choosing between Sanity and WordPress as a headless CMS is rarely a pure technical question. It involves editor habits, hosting budgets, plugin dependencies, and how much custom work you want to own long-term. I migrated a mid-size marketing site — 400 pages, 3 content editors, ~180k monthly visits — from WordPress (with WPGraphQL) to Sanity + Next.js earlier this year. The numbers below come from that project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editor UX: familiarity vs fit-for-purpose
&lt;/h2&gt;

&lt;p&gt;WordPress's editor is genuinely good for people who learned content editing on WordPress. Gutenberg's block library is wide, the media library is tactile, and non-technical editors can add pages without any developer help. That's a real advantage. When I've handed off WordPress sites to marketing teams who already know the platform, training time is near zero.&lt;/p&gt;

&lt;p&gt;Sanity Studio is a different experience. The editing surface is structured rather than visual — you fill in typed fields rather than assembling blocks freehand. Editors who come from a publishing or ops background adapt quickly because it feels more like a form than a page canvas. Editors who came from Gutenberg needed about two weeks to stop reaching for the block inserter. The payoff is that the content model is explicit: a "hero" is always a hero, a "testimonial" always has a quote and an author reference, and GROQ queries stay predictable.&lt;/p&gt;

&lt;p&gt;For the migration project, I ran both studios in parallel for three weeks. After go-live, none of the three editors asked to go back. The structured model reduced publishing errors — they'd previously broken layouts by pasting rich text with inline styles into heading fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: the gap is real
&lt;/h2&gt;

&lt;p&gt;On the old WordPress + WPGraphQL stack, the site averaged 2.8 s LCP on mobile (Lighthouse CI, median of 30 runs, same test pages). After migrating to Sanity + Next.js App Router with ISR, the same pages averaged 1.1 s LCP. TTFB dropped from 480 ms to 60 ms once pages were edge-cached on Vercel.&lt;/p&gt;

&lt;p&gt;WordPress headless is not inherently slow — WPGraphQL is capable, and a well-cached WordPress API can perform well. But WordPress still loads PHP, initialises a plugin stack, and hits MySQL on cache misses. Sanity's CDN-backed Content Delivery API returns JSON from an edge node. The ceiling is higher with Sanity once you invest in the query layer.&lt;/p&gt;

&lt;p&gt;CLS was the other win. WordPress's media library stores dimensions inconsistently — plugins resize images and lose the originals. Pre-calculating crop dimensions from Sanity's asset metadata let me pass explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to &lt;code&gt;next/image&lt;/code&gt; on every image, eliminating layout shift entirely on image-heavy pages. That work is described in detail in a separate post; the short version is that Sanity stores the original dimensions in the asset document and you can project them at query time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience
&lt;/h2&gt;

&lt;p&gt;WordPress headless requires two distinct systems: the WordPress install (PHP, MySQL, plugins, theme files even if unused) and the front-end framework. You maintain both. WPGraphQL adds a third dependency. Schema changes require ACF or a custom plugin, which means PHP. TypeScript types for the API response are hand-written or generated from an introspection query that drifts the moment a plugin updates.&lt;/p&gt;

&lt;p&gt;Sanity's developer experience is TypeScript-native end to end. Schemas are TypeScript objects. Sanity TypeGen generates types from those schemas, so your GROQ query results are typed at compile time. The Studio is a React app you extend with your own components. All of this lives in your repo — one deployment target 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;// sanity/schemas/post.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;defineType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineField&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&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;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;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;Post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&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;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&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;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&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;title&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="nf"&gt;defineField&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;publishedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&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;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&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="c1"&gt;// npx sanity typegen generate → PostDocument type available in queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The friction point with Sanity is the content model itself. WordPress gives you posts and pages on day one with no schema design required. With Sanity you make decisions upfront — what document types do you need, how do references work, what goes in a global settings document. That's an upfront cost of roughly 8–12 hours on a medium-complexity site, but it pays back quickly in query predictability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugin ecosystem: WordPress wins, with caveats
&lt;/h2&gt;

&lt;p&gt;WordPress has ~60,000 plugins. Sanity has an ecosystem of official and community plugins (Mux video, Cloudinary, desk tools, internationalization) but it's much smaller. If you need a specific integration — a particular payment gateway, a niche LMS, a regional shipping provider — WordPress probably has a plugin. Sanity probably requires custom code.&lt;/p&gt;

&lt;p&gt;The caveat is that WordPress plugins have a security and maintenance overhead that compounds. The migration project had 31 active plugins. Twelve of them existed to work around Gutenberg limitations or patch other plugins. Two had unfixed CVEs. Trimming to Sanity + a handful of focused API integrations reduced the attack surface substantially and removed a recurring maintenance cost the client had been absorbing as "normal".&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting cost and total cost of ownership
&lt;/h2&gt;

&lt;p&gt;WordPress hosting with WP Engine (Growth plan) was running the client £65/month. Vercel Pro for the Next.js front-end added £18/month. Sanity's free tier covered their usage (under 10k API calls/day). Total after migration: £18/month ongoing, with Sanity free, versus £65/month before.&lt;/p&gt;

&lt;p&gt;That's not always the outcome. High-traffic WordPress sites on shared hosting cost less than Vercel Pro. And the migration itself took 6 weeks of developer time — a one-time cost that only makes sense if you're planning to hold the site for 18+ months or if the WordPress maintenance overhead is already significant. I won't pretend Sanity is the cheaper option for a five-page brochure site that's been running on a £10/month shared host for three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stay on WordPress
&lt;/h2&gt;

&lt;p&gt;WordPress headless makes sense when your team already runs WordPress at scale, when you have heavy WooCommerce dependencies, or when the editor team is large and deeply invested in Gutenberg. WordPress's user management, multisite, and comment/membership ecosystems are genuinely hard to replicate in a custom Sanity setup without third-party services.&lt;/p&gt;

&lt;p&gt;WordPress also wins for projects with a tight timeline and a developer who knows the stack. A skilled WordPress developer can ship a new marketing site faster than a Sanity project if the Sanity content model needs to be designed from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Sanity headless is the right call
&lt;/h2&gt;

&lt;p&gt;Sanity earns its place when the content model is complex and needs to stay consistent across multiple front-ends (web, app, email), when the team wants TypeScript discipline across the CMS and the front-end, or when long-term maintenance cost matters more than setup speed. The performance ceiling is also meaningfully higher — not because Sanity's API is magic, but because the delivery layer is designed for edge caching from the start, whereas WordPress was designed for server rendering and has been adapted for headless use after the fact.&lt;/p&gt;

&lt;p&gt;The migration numbers are real: 60% reduction in LCP, 87% reduction in TTFB on cached pages, and a hosting bill that dropped by more than half. That's a strong case — but it took six weeks to get there, and it would not have been worth it on a shorter-lived project.&lt;/p&gt;

</description>
      <category>sanity</category>
      <category>wordpress</category>
      <category>headlesscms</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
