<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nayan Kyada</title>
    <description>The latest articles on DEV Community by Nayan Kyada (@nayankyada).</description>
    <link>https://dev.to/nayankyada</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2638501%2Fb0cc4a93-db40-4087-9ff8-b4c2debac8a1.jpg</url>
      <title>DEV Community: Nayan Kyada</title>
      <link>https://dev.to/nayankyada</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nayankyada"/>
    <language>en</language>
    <item>
      <title>Migrate Next.js Pages Router to App Router: A Buyer's Guide</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:09:47 +0000</pubDate>
      <link>https://dev.to/nayankyada/migrate-nextjs-pages-router-to-app-router-a-buyers-guide-3hbi</link>
      <guid>https://dev.to/nayankyada/migrate-nextjs-pages-router-to-app-router-a-buyers-guide-3hbi</guid>
      <description>&lt;p&gt;If you need to migrate a Next.js Pages Router site to the App Router, the good news is that you almost never have to do it all at once. Next.js lets both routers live in the same project, so a Sanity-powered marketing site can move one route at a time while staying live and ranking. The real questions are whether it is worth doing, in what order, and what can go wrong with search traffic.&lt;/p&gt;

&lt;p&gt;If you are coming from Gatsby instead, that is a different path, covered in &lt;a href="https://nayankyada.com/blog/migrate-gatsby-to-nextjs-app-router-what-the-path-actually-looks-like" rel="noopener noreferrer"&gt;migrating Gatsby to the Next.js App Router&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the migration is worth it
&lt;/h2&gt;

&lt;p&gt;The Pages Router is not broken. A site on it that is fast, stable and easy for editors to use does not need a rewrite for its own sake. I would migrate when one or more of these is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client JavaScript is heavy because every page hydrates in full, and Core Web Vitals (especially INP) suffer for it.&lt;/li&gt;
&lt;li&gt;You want streaming, Suspense boundaries, or partial prerendering, which only exist in the App Router model.&lt;/li&gt;
&lt;li&gt;New features keep fighting the old structure: nested layouts, per-section loading states, or draft preview done awkwardly with the older preview mode.&lt;/li&gt;
&lt;li&gt;Hiring is getting harder. Most developers now learn the App Router first, and library docs have moved that way.&lt;/li&gt;
&lt;li&gt;You are about to do a redesign or CMS change anyway, so the routing work rides along with work you are already paying for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not migrate just because a blog post said you should. If the site earns money and nothing above applies, leave it alone and put the budget into content or a &lt;a href="https://dev.to/services#packages"&gt;performance and Sanity audit&lt;/a&gt;, which starts from $800 and tells you what is actually slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incremental route-by-route strategy
&lt;/h2&gt;

&lt;p&gt;The safe approach is boring on purpose. You add an &lt;code&gt;app&lt;/code&gt; directory next to &lt;code&gt;pages&lt;/code&gt;, move routes across in order of risk, and never change a URL.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade first, migrate second.&lt;/strong&gt; Get to Next.js 16 and React 19 on the existing Pages Router code. Fix upgrade breakage separately so you know what caused what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the root layout.&lt;/strong&gt; The &lt;code&gt;app/layout.tsx&lt;/code&gt; file replaces &lt;code&gt;_app&lt;/code&gt; and &lt;code&gt;_document&lt;/code&gt;. Move fonts, global CSS and analytics there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move low-risk routes first.&lt;/strong&gt; A privacy policy, a contact page, a 404. These prove the layout, styling and metadata setup without risking revenue pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move templated content next.&lt;/strong&gt; Blog posts and landing pages built from Sanity documents. One template covers hundreds of URLs, so this is where the payoff is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move complex routes last.&lt;/strong&gt; Search, forms, authenticated areas, anything with heavy client state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the old file the moment the new one ships.&lt;/strong&gt; A URL cannot be served by both routers, and the build will complain if you forget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step is a separate deploy with its own staging URL. If a route misbehaves, you revert one file, not a quarter of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data fetching changes with Sanity
&lt;/h2&gt;

&lt;p&gt;This is where most of the actual rewriting happens. In the Pages Router you fetch in &lt;code&gt;getStaticProps&lt;/code&gt; and pass props down. In the App Router, a server component fetches directly and the data never has to be serialised into page props.&lt;/p&gt;

&lt;p&gt;Before:&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;// pages/blog/[slug].tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GetStaticProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPaths&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity/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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`*[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0]{title, body, publishedAt}`&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;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`*[_type == "post"].slug.current`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="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="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;notFound&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notFound&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity/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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`*[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0]{title, body, publishedAt}`&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`*[_type == "post"].slug.current`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;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="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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="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="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="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* render post */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&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;Three things change that clients should know about. First, &lt;code&gt;params&lt;/code&gt; is now a promise in current versions, so every dynamic route needs an &lt;code&gt;await&lt;/code&gt;. Second, time-based &lt;code&gt;revalidate&lt;/code&gt; is replaced by tag or path revalidation, which pairs well with Sanity webhooks; see &lt;a href="https://nayankyada.com/blog/how-i-wire-sanity-webhooks-to-nextjs-isr-revalidation-with-hmac-verification" rel="noopener noreferrer"&gt;Sanity webhooks with ISR revalidation&lt;/a&gt;. Third, draft preview moves to Draft Mode, covered in &lt;a href="https://nayankyada.com/blog/sanity-live-preview-nextjs-app-router-presentation-tool" rel="noopener noreferrer"&gt;Sanity Live Preview in the App Router&lt;/a&gt;. Editors will notice preview behaviour, so test it with them, not just with a developer.&lt;/p&gt;

&lt;p&gt;GROQ queries themselves usually do not change at all. That is the nice part: the content layer stays put while the rendering layer is replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO risks: metadata and canonicals
&lt;/h2&gt;

&lt;p&gt;Search traffic is the thing a migration can actually lose. The risks are specific and checkable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata.&lt;/strong&gt; &lt;code&gt;next/head&lt;/code&gt; does not work in the App Router. Each route needs &lt;code&gt;generateMetadata&lt;/code&gt; or a static &lt;code&gt;metadata&lt;/code&gt; export. A route that moves without one ships with no title or description, and nobody notices until rankings slip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonicals.&lt;/strong&gt; Set &lt;code&gt;alternates.canonical&lt;/code&gt; explicitly per route. Do not rely on defaults, especially on paginated or filtered pages, and keep trailing-slash behaviour the same as before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data.&lt;/strong&gt; JSON-LD that lived in a head component has to be rendered in the page body now. See &lt;a href="https://nayankyada.com/blog/nextjs-seo-adding-json-ld-structured-data-with-schemaorg-schemas" rel="noopener noreferrer"&gt;JSON-LD in Next.js&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects and status codes.&lt;/strong&gt; Missing documents must return a real 404 through &lt;code&gt;notFound()&lt;/code&gt;, not a 200 with an error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap and robots.&lt;/strong&gt; Move these to the &lt;code&gt;sitemap.ts&lt;/code&gt; and &lt;code&gt;robots.ts&lt;/code&gt; file conventions and diff the output against the old files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple generateMetadata looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0]{title, seoDescription}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;seoDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before each route goes live, crawl staging and production and compare title, description, canonical, robots meta, H1 and structured data. A spreadsheet diff catches nearly everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effort ranges by site size
&lt;/h2&gt;

&lt;p&gt;These are rough ranges from general practice, not quotes. Complexity matters more than page count, because a hundred posts on one template is cheap and ten bespoke interactive pages is not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site size&lt;/th&gt;
&lt;th&gt;Typical scope&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small (5 to 15 routes)&lt;/td&gt;
&lt;td&gt;Marketing pages, a blog, a contact form&lt;/td&gt;
&lt;td&gt;1 to 2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium (15 to 60 routes)&lt;/td&gt;
&lt;td&gt;Several templates, Sanity-driven pages, preview, redirects&lt;/td&gt;
&lt;td&gt;3 to 6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large (60+ routes or app features)&lt;/td&gt;
&lt;td&gt;Search, auth, i18n, custom API routes, heavy client state&lt;/td&gt;
&lt;td&gt;2 to 4 months, often phased&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Things that push you up: internationalised routing, a lot of &lt;code&gt;getServerSideProps&lt;/code&gt; logic, API routes that should become route handlers, a UI library that does not support server components, and no tests. Things that push you down: a clean Sanity schema, TypeScript throughout, and a staging environment that already exists.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Will every URL stay identical, and how will you prove it?&lt;/li&gt;
&lt;li&gt;How will you compare metadata and canonicals before and after each route?&lt;/li&gt;
&lt;li&gt;Which routes move first, and why those?&lt;/li&gt;
&lt;li&gt;How does draft preview work for editors afterwards?&lt;/li&gt;
&lt;li&gt;Can we deploy and roll back route by route?&lt;/li&gt;
&lt;li&gt;What do you do about client-side libraries that break in server components?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good answer is specific and mentions crawling staging. A vague answer about modernising the stack is a red flag.&lt;/p&gt;

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

&lt;p&gt;This is work I do: incremental App Router migrations on Sanity and Next.js sites, with an SEO diff on every route. If your site needs a rebuild alongside it, see my &lt;a href="https://dev.to/services#packages"&gt;Sanity and Next.js build and migration packages&lt;/a&gt;, or start with an audit to size the job.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>sanitycms</category>
      <category>seo</category>
    </item>
    <item>
      <title>Take Over an Existing Next.js Project After Your Developer Disappears</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:16:09 +0000</pubDate>
      <link>https://dev.to/nayankyada/take-over-an-existing-nextjs-project-after-your-developer-disappears-48he</link>
      <guid>https://dev.to/nayankyada/take-over-an-existing-nextjs-project-after-your-developer-disappears-48he</guid>
      <description>&lt;p&gt;You own a Next.js and Sanity site, and the person who built it has stopped replying. You need to take over the existing Next.js project without breaking what is live, and without finding out three weeks in that the Sanity project belongs to someone's personal email. This is the order I work in: access first, then a read-only audit, then a decision about repair or rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get ownership sorted before anyone writes code
&lt;/h2&gt;

&lt;p&gt;The code is the least urgent part. Accounts are what can lock you out, so list every system the site touches and write down who the owner is on each one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;What to confirm&lt;/th&gt;
&lt;th&gt;Who must be owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain registrar&lt;/td&gt;
&lt;td&gt;Account email, auto-renew, registrar lock&lt;/td&gt;
&lt;td&gt;Your company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Where nameservers point, who can edit records&lt;/td&gt;
&lt;td&gt;Your company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git host&lt;/td&gt;
&lt;td&gt;Repository owner, branch protection, collaborators&lt;/td&gt;
&lt;td&gt;Your organisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel (or other host)&lt;/td&gt;
&lt;td&gt;Team vs personal account, billing owner, env vars&lt;/td&gt;
&lt;td&gt;Your team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity project&lt;/td&gt;
&lt;td&gt;Administrator role, API tokens, CORS origins&lt;/td&gt;
&lt;td&gt;At least two of your people&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email and forms&lt;/td&gt;
&lt;td&gt;SendGrid or similar, sender domain, API keys&lt;/td&gt;
&lt;td&gt;Your company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics and Search Console&lt;/td&gt;
&lt;td&gt;Property owners&lt;/td&gt;
&lt;td&gt;Your company&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The domain is the one that matters most. If the registrar account is yours, you can always repoint the site. If it is not, fix that before anything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;Check whether the project lives under a team or under the developer's personal account. A project in a personal account can usually be transferred to a team, but this requires the developer's cooperation or a support request. If you cannot get that, the fallback is deploying the repository to a new project under your team and moving the domain across. That is why having the code matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sanity
&lt;/h3&gt;

&lt;p&gt;Only an administrator can add members or change roles. Open the project's members page in the Sanity management console and see who holds that role. If it is only the departed developer, contact Sanity support with proof of business ownership. Do not wait: the content is the one asset you cannot regenerate from a repository.&lt;/p&gt;

&lt;p&gt;While you are there, list the API tokens. Anything with write access that you did not create should be rotated once you have control. Check the CORS origins and the webhook list too; stale webhooks pointing at old preview URLs are common.&lt;/p&gt;

&lt;h3&gt;
  
  
  The repository
&lt;/h3&gt;

&lt;p&gt;If the repo is in your organisation, good. If it is in the developer's personal account, ask for a transfer or a full mirror including all branches and tags. If you have neither, look for the code in the hosting provider: Vercel keeps deployments, and the connected Git repository is shown in project settings. That tells you where to look, even if it does not give you access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first-week audit checklist
&lt;/h2&gt;

&lt;p&gt;The audit is read-only. Do not upgrade packages, do not clean up, and do not push to the production branch. You are building a picture of risk.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can it run locally?&lt;/strong&gt; Clone, install, copy env vars from the host, start the dev server. If this takes more than a day, that is already your answer about maintainability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can it build in CI or on Vercel from a clean checkout?&lt;/strong&gt; A site that only deploys from one laptop is a liability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which Next.js version and router?&lt;/strong&gt; Note the major version, whether it uses the App Router, Pages Router, or both, and whether the build output shows pages that are statically generated or fully dynamic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What does the content model look like?&lt;/strong&gt; Open the Studio and read the schema folder. Count document types, check for unused ones, and check whether references are used or content is duplicated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables.&lt;/strong&gt; List every one the app reads. Mark which are secrets and which are missing from the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data fetching.&lt;/strong&gt; Search for how GROQ queries are written and whether the client uses the CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revalidation.&lt;/strong&gt; Find out how content updates reach the live site: webhooks, time-based revalidation, or nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance baseline.&lt;/strong&gt; Run Lighthouse and look at field data in Search Console. Record it before you touch anything so you can show improvement later. My post on &lt;a href="https://nayankyada.com/blog/how-i-set-up-core-web-vitals-monitoring-with-vercel-analytics-and-nextjs" rel="noopener noreferrer"&gt;Core Web Vitals monitoring&lt;/a&gt; covers the setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO state.&lt;/strong&gt; Check the sitemap, robots, canonical tags, and redirects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A quick way to start the inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# run from the repo root&lt;/span&gt;
git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"12 months ago"&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s2"&gt;"%an %ad %s"&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"process.env"&lt;/span&gt; src app lib &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.ts &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.tsx | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*process.env.//'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
npm outdated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The git log tells you whether work was steady or abandoned, the grep gives a rough env var list, and &lt;code&gt;npm outdated&lt;/code&gt; shows how far behind dependencies are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags in an inherited Next.js and Sanity codebase
&lt;/h2&gt;

&lt;p&gt;None of these is fatal alone. Several together should change your plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No lockfile, or two of them.&lt;/strong&gt; Builds are not reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets committed to the repo.&lt;/strong&gt; Rotate them immediately, whatever else you decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Sanity write token used in client-side code.&lt;/strong&gt; Anything prefixed for the browser is public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-coded content in components&lt;/strong&gt; while the Studio holds unused equivalents. Editors will change things that do nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetching everything on every request&lt;/strong&gt; with no caching strategy, which usually shows up as slow pages and a large Sanity bill. See &lt;a href="https://nayankyada.com/blog/how-i-model-sanity-references-to-avoid-n1-queries-in-nextjs-rsc" rel="noopener noreferrer"&gt;avoiding N+1 queries with Sanity references&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No types.&lt;/strong&gt; Plain JavaScript with unvalidated query results means every change is a guess. TypeGen fixes much of this, as in &lt;a href="https://nayankyada.com/blog/how-i-set-up-sanity-typegen-for-fully-typed-groq-queries-in-typescript" rel="noopener noreferrer"&gt;fully typed GROQ queries&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A very old Studio.&lt;/strong&gt; If the Studio is on v2 or v3, it needs to move to the current major. The path is in &lt;a href="https://nayankyada.com/blog/sanity-studio-v2-to-v3-upgrade-what-breaks-and-how-to-fix-it" rel="noopener noreferrer"&gt;the Studio upgrade post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No preview or an exposed one.&lt;/strong&gt; Draft content visible to anyone with the URL is a quiet but real problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dozens of unused document types&lt;/strong&gt; that confuse editors and slow the Studio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful check for the token problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"SANITY_API_TOKEN&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;NEXT_PUBLIC_.*TOKEN"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.ts &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.tsx &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any token with a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix is shipped to every visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild or repair
&lt;/h2&gt;

&lt;p&gt;I decide this on four questions, not on how much I dislike the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repair&lt;/strong&gt; when the site runs locally, builds cleanly, uses a reasonable content model, and the pain is limited to performance, a few bugs, or missing features. Upgrading Next.js and fixing hot spots is cheaper than a rebuild nearly every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repair the Studio, rebuild the front end&lt;/strong&gt; when the content is healthy but the Next.js code is a tangle. The Sanity dataset is the valuable asset. A new front end can read the same content with better queries. This is the most common outcome I see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebuild both&lt;/strong&gt; when the schema is the problem: duplicated content, no references, fields named after pages rather than meaning. Migrating content into a clean model costs more up front, but it stops every future change from being expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move platforms&lt;/strong&gt; when the site is on Gatsby; see &lt;a href="https://nayankyada.com/blog/migrate-gatsby-to-nextjs-app-router-what-the-path-actually-looks-like" rel="noopener noreferrer"&gt;migrating Gatsby to the App Router&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are comparing the cost of a rebuild against a series of patches, be wary of anyone who quotes without opening the codebase.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Will you audit before quoting, and will the audit be written down?&lt;/li&gt;
&lt;li&gt;Where will work happen: a staging URL and a branch, not production?&lt;/li&gt;
&lt;li&gt;Who will own the accounts at the end?&lt;/li&gt;
&lt;li&gt;How will you handle secrets you find in the repo?&lt;/li&gt;
&lt;li&gt;What is the rollback plan if a deploy goes wrong?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://nayankyada.com/blog/how-to-evaluate-a-sanity-cms-development-services-proposal-before-you-sign" rel="noopener noreferrer"&gt;proposal evaluation guide&lt;/a&gt; covers the contract side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work Nayan does
&lt;/h2&gt;

&lt;p&gt;Taking over abandoned Next.js and Sanity sites is work I do. A fixed-price &lt;a href="https://dev.to/services#packages"&gt;performance and Sanity audit&lt;/a&gt; starts from $800 and gives you the written risk list, account ownership steps, and a repair-or-rebuild recommendation before you commit to anything larger.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sanitycms</category>
      <category>approuter</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Contentful to Sanity Migration: How I Move Content, Assets and Locales</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:38:11 +0000</pubDate>
      <link>https://dev.to/nayankyada/contentful-to-sanity-migration-how-i-move-content-assets-and-locales-2le7</link>
      <guid>https://dev.to/nayankyada/contentful-to-sanity-migration-how-i-move-content-assets-and-locales-2le7</guid>
      <description>&lt;p&gt;A contentful to sanity migration is mostly a data-mapping exercise with a few sharp edges: rich text, assets, and locales. Done as a script you can re-run, it is low drama. Done by hand in a CMS UI, it becomes a month of copy-pasting. This is the order I work in.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Contentful bill is the trigger
&lt;/h2&gt;

&lt;p&gt;Most migrations I see start with an invoice, not a technical complaint. A site outgrows the free tier, hits the jump to a paid plan, or needs a second environment or more locales, and the number stops making sense. If that is your situation, read &lt;a href="https://nayankyada.com/blog/contentful-pricing-2026-free-tier-limits-the-300-cliff-and-enterprise" rel="noopener noreferrer"&gt;Contentful pricing in 2026&lt;/a&gt; first so you know exactly which limit you hit, and then &lt;a href="https://nayankyada.com/blog/sanity-cms-vs-contentful-for-nextjs-projects-an-honest-comparison" rel="noopener noreferrer"&gt;Sanity vs Contentful for Next.js&lt;/a&gt; to check cost is not the only reason you are moving. Cost alone is a fine reason, but it should not be the only input into how you remodel content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Export and audit what you have
&lt;/h2&gt;

&lt;p&gt;Use the Contentful CLI to export a space to JSON. That file contains content types, entries, assets and locales. Before writing any code, list every content type and mark each one: keep, merge, or drop. Migrations are the one moment you can delete the dead types nobody has touched in two years.&lt;/p&gt;

&lt;p&gt;I produce a simple spreadsheet: content type, entry count, fields, which pages use it. Anything with zero entries or no front-end consumer gets dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Map the content model
&lt;/h2&gt;

&lt;p&gt;Contentful models map onto Sanity schemas fairly directly, but a few concepts change shape.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contentful&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Content type&lt;/td&gt;
&lt;td&gt;Document type or object type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short text / long text&lt;/td&gt;
&lt;td&gt;string / text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich text&lt;/td&gt;
&lt;td&gt;array of block (Portable Text)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media (asset)&lt;/td&gt;
&lt;td&gt;image or file field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference (link to entry)&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many references&lt;/td&gt;
&lt;td&gt;array of references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locale per field&lt;/td&gt;
&lt;td&gt;Field-level or document-level i18n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entry ID&lt;/td&gt;
&lt;td&gt;Document _id you control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The main decision: which Contentful types become standalone documents and which become inline objects. A Contentful "Hero" entry referenced by one page is almost always better as an object inside the page. Reusable things like authors and categories stay as documents. Structure this with query cost in mind; &lt;a href="https://nayankyada.com/blog/how-i-structure-sanity-schemas-to-avoid-query-waterfalls-in-nextjs" rel="noopener noreferrer"&gt;avoiding query waterfalls&lt;/a&gt; covers the reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Use deterministic IDs
&lt;/h2&gt;

&lt;p&gt;The single most useful trick is generating Sanity document IDs from Contentful IDs. That makes the import idempotent, so you can re-run it as often as you like, and references can be written before the target document exists.&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;// scripts/migrate/ids.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentfulId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentfulId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;refTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentfulId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="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;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentfulId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;_weak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I mark references weak during import so ordering does not matter, then strengthen them once everything has landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Convert rich text to Portable Text
&lt;/h2&gt;

&lt;p&gt;Contentful rich text is a tree: a document node with block children (paragraph, headings, lists, quotes, embedded entries), and text nodes carrying marks like bold or italic. Portable Text is a flat array of blocks with spans. The conversion is a recursive walk.&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;// scripts/migrate/richText.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;randomUUID&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;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CfNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;nodeType&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;value&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;marks&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;CfNode&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;markMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;em&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;underline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;underline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styleMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&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;heading-2&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;h2&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;heading-3&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;h3&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;heading-4&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;h4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blockquote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blockquote&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CfNode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;markDefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;_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;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt; &lt;span class="o"&gt;??&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;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;markMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hyperlink&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;markDefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="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;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&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;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markDefs&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="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markDefs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toPortableText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CfNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styleMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&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;style&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;markDefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;return&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="na"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;markDefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markDefs&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;This handles paragraphs, headings, quotes, bold, italic and links. Lists, tables, embedded entries and embedded assets need their own branches: lists become blocks with listItem and level, embedded entries become custom block objects. I deliberately skip unknown node types in this sketch, but in a real run you should log them, because silent drops are how content goes missing. Rendering the result is covered in &lt;a href="https://nayankyada.com/blog/how-i-build-sanity-portable-text-custom-components-in-nextjs" rel="noopener noreferrer"&gt;Portable Text custom components&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Move assets
&lt;/h2&gt;

&lt;p&gt;Contentful assets have a URL per locale. Download each file once, upload it to Sanity with the client, and keep a map from Contentful asset ID to Sanity asset ID. Then, when converting entries, replace each asset link with an image field pointing at that asset ID.&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;// scripts/migrate/assets.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SANITY_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SANITY_WRITE_TOKEN&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uploadAsset&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;//&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="s2"&gt;`https:&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="s2"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filename&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;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Throttle uploads and cache the ID map to disk so a failed run resumes instead of starting over. Add alt text to the image fields in the new schema; Contentful asset descriptions are a good source for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Handle locales
&lt;/h2&gt;

&lt;p&gt;Contentful stores every locale inside each field. Sanity gives you two sensible options: field-level translations for small sites, or one document per language using the document internationalization plugin for larger ones. For editorial sites with divergent content per market I prefer one document per language. Write a script that splits each Contentful entry into one document per locale, with a shared language field and deterministic IDs like page-abc123-en. The front-end side is covered in &lt;a href="https://nayankyada.com/blog/how-i-wire-sanity-cms-multilingual-content-to-nextjs-with-next-intl" rel="noopener noreferrer"&gt;multilingual Sanity content with next-intl&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Run the import as a transaction batch
&lt;/h2&gt;

&lt;p&gt;Build an array of documents, then write them in batches using createOrReplace so reruns overwrite instead of duplicating. Import into a separate dataset first, such as staging, and let editors review there. Only copy to production when the checks pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Build the redirect map
&lt;/h2&gt;

&lt;p&gt;Export every live URL from your sitemap and analytics, then map each to its new path. If you keep slugs identical, most of the map is empty, which is the ideal outcome. Where paths change, add permanent redirects in the Next.js config or in middleware. Crawl the old URL list against the new site after launch and confirm every one returns either a 200 or a single 301 hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Plan the cutover
&lt;/h2&gt;

&lt;p&gt;My cutover order is: freeze edits in Contentful, run the final import, run the redirect and link crawl, switch DNS or the production deployment, then keep Contentful on its plan for a couple of weeks as a fallback before cancelling. Configure &lt;a href="https://nayankyada.com/blog/how-i-wire-sanity-webhooks-to-nextjs-isr-revalidation-with-hmac-verification" rel="noopener noreferrer"&gt;webhook revalidation&lt;/a&gt; before launch so editors see changes immediately.&lt;/p&gt;

&lt;p&gt;If you want this handled end to end, my fixed-price migration to Sanity starts from $3,000, with details at &lt;a href="https://dev.to/services#packages"&gt;services and packages&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>portabletext</category>
      <category>internationalization</category>
    </item>
    <item>
      <title>Sanity Studio v2 to v3 upgrade (and on to v6): what breaks and how to fix it</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:00:44 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-studio-v2-to-v3-upgrade-what-breaks-and-how-to-fix-it-3l3a</link>
      <guid>https://dev.to/nayankyada/sanity-studio-v2-to-v3-upgrade-what-breaks-and-how-to-fix-it-3l3a</guid>
      <description>&lt;p&gt;If your team is still on Sanity Studio v2, you are running a legacy Studio that Sanity no longer develops. The Sanity Studio v2 to v3 upgrade is the hard part — the configuration model, plugin API, and custom input system were all rewritten — but in 2026 you should not stop at v3. The current major version is v6, and once you have done the v3 rewrite, the remaining steps are small. This post covers what breaks, what the risk of waiting is, how to approach the migration, and what to ask a developer before hiring someone to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why v2 is no longer safe to leave alone
&lt;/h2&gt;

&lt;p&gt;Studio v2 no longer receives updates. It is a deployed web app — self-hosted or at &lt;code&gt;yourproject.sanity.studio&lt;/code&gt; — built on an older React version and older build tooling, and those dependencies keep accumulating known vulnerabilities with nobody patching them. Your content is stored in Sanity's cloud and is not directly at risk, but the Studio is an authenticated surface: a vulnerability that allows script injection in the editor UI could let an attacker act as a logged-in editor.&lt;/p&gt;

&lt;p&gt;Beyond security, everything new in Sanity is built for the modern Studio. &lt;a href="https://nayankyada.com/blog/how-i-set-up-sanity-ai-assist-for-editors-in-a-nextjs-project" rel="noopener noreferrer"&gt;AI Assist&lt;/a&gt;, the &lt;a href="https://nayankyada.com/blog/sanity-live-preview-nextjs-app-router-presentation-tool" rel="noopener noreferrer"&gt;Presentation tool for live preview&lt;/a&gt;, &lt;a href="https://nayankyada.com/blog/how-i-set-up-sanity-typegen-for-fully-typed-groq-queries-in-typescript" rel="noopener noreferrer"&gt;Sanity TypeGen&lt;/a&gt;, auto-updating Studio deployments, and the improved search all require v3 or later. Every month on v2 is a month your editors go without them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks in the v2 to v3 upgrade
&lt;/h2&gt;

&lt;p&gt;The v2 to v3 jump is a ground-up rewrite of the Studio. The Content Lake API did not change — your GROQ queries, your dataset, and your documents are untouched. What changed is everything from the configuration layer up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration format.&lt;/strong&gt; v2 used a &lt;code&gt;sanity.json&lt;/code&gt; manifest and a plugin-based folder structure. The modern Studio uses a single &lt;code&gt;sanity.config.ts&lt;/code&gt; file with a &lt;code&gt;defineConfig()&lt;/code&gt; call. There is no automatic converter from one to the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desk structure.&lt;/strong&gt; The v2 desk tool is now the Structure tool: &lt;code&gt;structureTool()&lt;/code&gt; imported from &lt;code&gt;sanity/structure&lt;/code&gt;. The structure builder (&lt;code&gt;S.list()&lt;/code&gt;, &lt;code&gt;S.documentTypeList()&lt;/code&gt;, &lt;code&gt;.child()&lt;/code&gt;) will look familiar, but you register it through the plugin's &lt;code&gt;structure&lt;/code&gt; option instead of a parts override, and any custom panes need testing. If you have a complex desk, my notes on &lt;a href="https://nayankyada.com/blog/how-i-customise-sanity-structure-builder-with-custom-ordering-and-grouping" rel="noopener noreferrer"&gt;custom ordering and grouping in Structure Builder&lt;/a&gt; apply directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugins.&lt;/strong&gt; Every v2 plugin needs a modern equivalent. Common ones like Vision (&lt;code&gt;@sanity/vision&lt;/code&gt;) have been rebuilt, but package names, import paths, and configuration options changed, and some community plugins were never ported. Audit your plugin list early — an abandoned plugin can be the biggest unknown in the estimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom inputs.&lt;/strong&gt; This is the most common source of pain. v2 inputs used &lt;code&gt;PatchEvent&lt;/code&gt; and &lt;code&gt;withDocument&lt;/code&gt; from the old form builder parts. Modern inputs are plain React components that receive props, call &lt;code&gt;props.onChange(set(value))&lt;/code&gt; or &lt;code&gt;unset()&lt;/code&gt;, read other fields with &lt;code&gt;useFormValue&lt;/code&gt;, and can wrap the default input with &lt;code&gt;props.renderDefault(props)&lt;/code&gt;. Every custom input is a rewrite, not a re-import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parts system gone.&lt;/strong&gt; v2's &lt;code&gt;parts&lt;/code&gt; system for overriding Studio internals no longer exists. Logos, navbar tweaks, and custom document views move to explicit options and component overrides in &lt;code&gt;defineConfig()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema helpers.&lt;/strong&gt; The &lt;code&gt;sanity&lt;/code&gt; package exports &lt;code&gt;defineType&lt;/code&gt;, &lt;code&gt;defineField&lt;/code&gt;, and &lt;code&gt;defineArrayMember&lt;/code&gt; for type-safe schemas. Plain object schemas still work, so you can convert gradually — but it is worth doing during the rewrite, because TypeGen and editor autocomplete both benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't stop at v3: what changes on the way to v6
&lt;/h2&gt;

&lt;p&gt;Once your Studio runs on the v3 configuration model, the later majors are mostly about runtime requirements. If you bootstrap the new Studio with &lt;code&gt;npm create sanity@latest&lt;/code&gt;, you start on the current version and skip these steps entirely. If you port an existing v3 codebase, here is what each major adds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Major&lt;/th&gt;
&lt;th&gt;What changes&lt;/th&gt;
&lt;th&gt;Typical effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;Node.js 20.19+ (or 22.12+) to run the CLI&lt;/td&gt;
&lt;td&gt;Minutes, if your Node.js is current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;React 19.2.2+ required; TypeGen renames result types for snake_case query names&lt;/td&gt;
&lt;td&gt;Hours — check custom inputs and plugins for React 19 compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;td&gt;Node.js 22.12+; React strict mode on in development; &lt;code&gt;auth.providers&lt;/code&gt; arrays replace the defaults; search defaults to the &lt;code&gt;groq2024&lt;/code&gt; strategy; build moves to Vite 8&lt;/td&gt;
&lt;td&gt;Hours, mostly testing custom inputs under strict mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your schemas, plugin configuration shape, and content APIs stay the same through all three. Sanity publishes a migration guide for each major, and the &lt;a href="https://www.sanity.io/docs/studio/upgrade" rel="noopener noreferrer"&gt;upgrade overview&lt;/a&gt; links them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade path by Studio size
&lt;/h2&gt;

&lt;p&gt;The effort scales with how much custom code sits on top of the default Studio.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Studio profile&lt;/th&gt;
&lt;th&gt;Custom inputs&lt;/th&gt;
&lt;th&gt;Custom structure&lt;/th&gt;
&lt;th&gt;Effort estimate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default schemas, stock plugins&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;2–4 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several document types, 1–2 custom inputs&lt;/td&gt;
&lt;td&gt;1–3&lt;/td&gt;
&lt;td&gt;Basic &lt;code&gt;S.list()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large schema set, multiple custom inputs, branded UI&lt;/td&gt;
&lt;td&gt;4+&lt;/td&gt;
&lt;td&gt;Complex nested structure&lt;/td&gt;
&lt;td&gt;3–5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-workspace, custom tools, unported plugins&lt;/td&gt;
&lt;td&gt;Many&lt;/td&gt;
&lt;td&gt;Multiple workspaces and tool tabs&lt;/td&gt;
&lt;td&gt;1–2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those estimates assume someone who has done the migration before, and they include landing on v6 rather than v3. If a developer is reading the migration guides for the first time on your project, add about half again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upgrade sequence that works
&lt;/h2&gt;

&lt;p&gt;Do not upgrade in place on the Studio your editors use every day. This sequence keeps v2 working until the new Studio has proven itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Branch the Studio codebase.&lt;/strong&gt; Keep v2 deployed for editors while you work on a separate branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap a clean, current Studio.&lt;/strong&gt; Run &lt;code&gt;npm create sanity@latest&lt;/code&gt; in a scratch folder so you start on the latest major, then port your schemas into it one document type at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port schemas first and check them with Vision.&lt;/strong&gt; Get every schema loading and query real documents before touching any UI customisation. Schemas are the lowest-risk layer — and a good moment to &lt;a href="https://nayankyada.com/blog/how-i-audit-and-prune-unused-sanity-document-types-to-reclaim-studio-performance" rel="noopener noreferrer"&gt;prune document types nobody uses&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild the structure.&lt;/strong&gt; Recreate your desk with &lt;code&gt;structureTool()&lt;/code&gt; and test each list and nested pane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite custom inputs one at a time.&lt;/strong&gt; Each is a self-contained rewrite; test it under React strict mode, which v6 turns on in development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace parts overrides.&lt;/strong&gt; Move logo, navbar, and document view customisations into &lt;code&gt;defineConfig()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let editors use it before cutover.&lt;/strong&gt; Deploy the new Studio to a separate URL against your production dataset — the content API is the same, so both Studios can run side by side — and give your content team a week or two of real use before switching the main URL.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sanity.config.ts — modern Studio shape (v3 and later, including v6)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;structureTool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sanity/structure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;visionTool&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/vision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schemaTypes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schemaTypes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="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;default&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;My Studio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;structureTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// replaces the v2 desk tool&lt;/span&gt;
    &lt;span class="nf"&gt;visionTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schemaTypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The v2 equivalent was spread across &lt;code&gt;sanity.json&lt;/code&gt;, a desk structure file, and parts declarations. One typed file is far easier to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a rewritten custom input looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// schemaTypes/inputs/SlugPreviewInput.tsx — modern custom input pattern&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFormValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StringInputProps&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;function&lt;/span&gt; &lt;span class="nf"&gt;SlugPreviewInput&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="nx"&gt;StringInputProps&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFormValue&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="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderDefault&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="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#888&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Slug preview: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The v2 version of this input would have reached for &lt;code&gt;PatchEvent&lt;/code&gt; and a &lt;code&gt;withDocument&lt;/code&gt; wrapper, neither of which exists in the modern Studio. For a fuller example, see how I &lt;a href="https://nayankyada.com/blog/how-i-build-a-sanity-studio-custom-input-component-with-a-colour-picker" rel="noopener noreferrer"&gt;build a custom input with a colour picker&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;If you are scoping this work with a freelancer or agency, these questions separate someone who has done it from someone learning on your budget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Will you land us on the current major version, or stop at v3 — and why?&lt;/li&gt;
&lt;li&gt;Can you show me a modern &lt;code&gt;sanity.config.ts&lt;/code&gt; from a project you have shipped?&lt;/li&gt;
&lt;li&gt;How will you handle the custom input rewrite, and how will you test it?&lt;/li&gt;
&lt;li&gt;Which of our plugins have no modern equivalent, and what is the plan for each?&lt;/li&gt;
&lt;li&gt;Will editors use the new Studio against production content before cutover, and is v2 kept as a fallback?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A developer who has done this before will answer all five without hedging — especially the plugin question and the rollback plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of another six months on v2
&lt;/h2&gt;

&lt;p&gt;Every month on v2 is another month of unpatched dependencies, another month without AI Assist, Presentation, or TypeGen, and more schema debt to untangle later. The upgrade is not trivial, but it is bounded work — usually days, not months, even for a Studio with custom inputs and a structured desk.&lt;/p&gt;

&lt;p&gt;This is work I do for teams on Sanity, from a quick &lt;a href="https://dev.to/services#packages"&gt;performance and Sanity audit&lt;/a&gt; that maps your Studio and plugins to a full fixed-price migration. If you have a v2 Studio — or inherited one and are not sure what state it is in — that audit is the fastest way to get a real estimate.&lt;/p&gt;

</description>
      <category>sanitystudio</category>
      <category>sanitycms</category>
      <category>typescript</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Migrate Gatsby to Next.js App Router: what the path actually looks like</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Mon, 12 Oct 2026 12:16:51 +0000</pubDate>
      <link>https://dev.to/nayankyada/migrate-gatsby-to-nextjs-app-router-what-the-path-actually-looks-like-3pf0</link>
      <guid>https://dev.to/nayankyada/migrate-gatsby-to-nextjs-app-router-what-the-path-actually-looks-like-3pf0</guid>
      <description>&lt;p&gt;If you're trying to migrate Gatsby to Next.js and you've landed here after a failed &lt;code&gt;npm install&lt;/code&gt; or a Node version conflict, you're not alone. Gatsby's plugin ecosystem quietly rotted over the past two years, and most teams are now sitting on sites they can't upgrade, can't deploy cleanly, and can't hand to a junior developer without a full briefing on which workarounds hold the whole thing together.&lt;/p&gt;

&lt;p&gt;This post covers the migration path I've used on real Gatsby + Sanity projects — what breaks, what carries over, and how to protect your URLs and your editors when you make the jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptoms that tell you it's time
&lt;/h2&gt;

&lt;p&gt;The clients who reach me at this point usually describe the same cluster of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node 18 or 20 breaks the build. &lt;code&gt;gatsby-plugin-sharp&lt;/code&gt;, &lt;code&gt;gatsby-source-sanity&lt;/code&gt;, or something deeper in the Gatsby bootstrap throws on modern Node. The workaround is pinning Node 16 in &lt;code&gt;.nvmrc&lt;/code&gt; and hoping the hosting platform still supports it.&lt;/li&gt;
&lt;li&gt;Plugin PRs go unanswered. Gatsby is technically "in maintenance mode" but in practice that means bugs sit open and the people who maintained the plugins have moved on.&lt;/li&gt;
&lt;li&gt;Build times are painful. Cold builds on a mid-size Sanity-backed Gatsby site often run 8–15 minutes because Gatsby pulls every document into a local GraphQL layer at build time.&lt;/li&gt;
&lt;li&gt;Incremental builds broke silently. The promise of Gatsby's incremental cache worked sometimes, in some CI environments, and now it mostly doesn't, and nobody can tell you why.&lt;/li&gt;
&lt;li&gt;The Studio is fine but the front end is the problem. Sanity v3 Studio is perfectly healthy — it's the Gatsby front end that's the liability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost of staying is real and it compounds. Every month you stay on a Node 16 pin is a month further from being able to adopt anything new. Every plugin update you skip because it might break the build is technical debt that lands on whoever inherits the codebase next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Gatsby + Sanity site actually looks like under the hood
&lt;/h2&gt;

&lt;p&gt;Before I get into the migration, it's worth saying what you're actually dealing with. A typical Gatsby + Sanity site has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gatsby-source-sanity&lt;/code&gt; pulling documents into Gatsby's GraphQL data layer&lt;/li&gt;
&lt;li&gt;Page creation in &lt;code&gt;gatsby-node.js&lt;/code&gt; using &lt;code&gt;createPage&lt;/code&gt; against GraphQL queries&lt;/li&gt;
&lt;li&gt;Image handling through &lt;code&gt;gatsby-plugin-image&lt;/code&gt; with Sanity's image source&lt;/li&gt;
&lt;li&gt;A mix of static pages and dynamically generated routes under a handful of URL patterns (&lt;code&gt;/blog/:slug&lt;/code&gt;, &lt;code&gt;/case-studies/:slug&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good news: Sanity itself is portable. Your schemas, your content, your Studio — none of that is coupled to Gatsby. You're migrating the rendering layer, not the CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration path I actually use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Audit before you touch anything (week 1)
&lt;/h3&gt;

&lt;p&gt;I start by crawling the live site with a tool like &lt;code&gt;wget --spider&lt;/code&gt; or Screaming Frog to get a complete URL inventory. Every path that exists in production goes into a spreadsheet. This becomes the URL parity checklist and the redirect map if anything changes shape.&lt;/p&gt;

&lt;p&gt;At the same time, I audit the Gatsby GraphQL queries in each page template. These become the GROQ queries in the Next.js version. The data requirements are the same — the query language changes.&lt;/p&gt;

&lt;p&gt;I also note which Gatsby plugins are doing real work versus which ones are handling things Next.js handles natively (image optimisation, font loading, sitemap). That shortens the dependency list considerably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Build the Next.js shell in parallel (weeks 2–3)
&lt;/h3&gt;

&lt;p&gt;I create the Next.js App Router project in a &lt;code&gt;/next&lt;/code&gt; subdirectory of the same repo, or in a separate branch — never replacing the Gatsby project until the new one is verified. The client's Sanity project ID and dataset go straight into &lt;code&gt;.env.local&lt;/code&gt;. The Sanity client setup is identical to any other Next.js + Sanity project.&lt;/p&gt;

&lt;p&gt;The route structure maps like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Gatsby: gatsby-node.js createPage for /blog/:slug&lt;/span&gt;
&lt;span class="c1"&gt;// Next.js App Router equivalent:&lt;/span&gt;
&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;

&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity/lib/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notFound&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == "post" &amp;amp;&amp;amp; defined(slug.current)]{ "slug": slug.current }`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;slugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`*[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0]{
      title, body, publishedAt,
      mainImage{ asset-&amp;gt;{ url, metadata { dimensions, lqip } }, hotspot, crop }
    }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// render&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern replaces the entire &lt;code&gt;gatsby-node.js&lt;/code&gt; &lt;code&gt;createPage&lt;/code&gt; loop. &lt;code&gt;generateStaticParams&lt;/code&gt; at build time, GROQ instead of GraphQL, &lt;code&gt;notFound()&lt;/code&gt; instead of a 404 redirect in the Gatsby config.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: URL parity and redirects (week 3)
&lt;/h3&gt;

&lt;p&gt;Gatsby's default trailing-slash behaviour differs from Next.js. If your Gatsby site served &lt;code&gt;/about/&lt;/code&gt; and Next.js serves &lt;code&gt;/about&lt;/code&gt;, search engines see them as different URLs and you'll haemorrhage rankings.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;next.config.ts&lt;/code&gt;, set &lt;code&gt;trailingSlash: true&lt;/code&gt; if your existing URLs use trailing slashes, or write explicit redirects for any paths that genuinely change shape during the migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;trailingSlash&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="c1"&gt;// example: Gatsby used /news/:slug, Next.js uses /blog/:slug&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;/news/: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;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/: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;permanent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before launch I run the URL inventory from phase 1 against the new site using a script that checks every path and logs non-200 responses. No path should 404 that didn't 404 before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: ISR instead of long builds (week 4)
&lt;/h3&gt;

&lt;p&gt;This is where the biggest operational win lands. Rather than rebuilding every page on every deploy, I set &lt;code&gt;revalidate&lt;/code&gt; on the data fetches that change frequently, and use Sanity webhooks to trigger on-demand revalidation for specific documents when editors publish. Build times drop from 12 minutes to under 2 minutes because the static generation at deploy time only handles the shell — content updates revalidate in the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Staging review and handoff
&lt;/h3&gt;

&lt;p&gt;I deploy the Next.js build to a Vercel preview URL, share it with the client for a two-week parallel run, and then do the DNS cutover once they've signed off. The Sanity Studio URL doesn't change. The editors notice nothing except that the front end is faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realistic timeline
&lt;/h2&gt;

&lt;p&gt;For a Gatsby + Sanity site with 5–10 content types and 200–500 pages, the migration runs 4–6 weeks: one week of audit, two weeks of build, one week of URL/redirect verification, one week of staging review. More complex sites with e-commerce, internationalisation, or heavy custom Gatsby plugins add 2–4 weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask a developer before you hire for this
&lt;/h2&gt;

&lt;p&gt;Ask them how they handle URL parity — if they say "redirects" without mentioning a pre-launch URL audit and automated checking, that's a gap. Ask whether they'll run the Gatsby and Next.js sites in parallel before cutover, or go straight to a swap. Ask how they'll test that every editor workflow still functions post-migration. The Sanity Studio is untouched but webhooks, preview links, and any custom document actions need verifying against the new front end.&lt;/p&gt;

&lt;p&gt;This is the kind of migration I run for clients — Gatsby to Next.js App Router, Sanity intact, URL parity confirmed before the DNS moves. If your Gatsby site is pinned to Node 16 and your builds are breaking, get in touch.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>sanitycms</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Sanity vs WordPress.org Pricing 2026: Real Hosting and Plugin Costs Compared</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:57:01 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-wordpressorg-pricing-2026-real-hosting-and-plugin-costs-compared-3j1b</link>
      <guid>https://dev.to/nayankyada/sanity-vs-wordpressorg-pricing-2026-real-hosting-and-plugin-costs-compared-3j1b</guid>
      <description>&lt;p&gt;Comparing Sanity vs WordPress.org pricing is harder than it looks because WordPress.org is self-hosted software — the license is free, but every cost you'd normally ignore compounds into a real number by year two. This post breaks down what each platform actually costs at small, medium, and growth scale, including infrastructure, plugins, and maintenance time you should price in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually paying for on each platform
&lt;/h2&gt;

&lt;p&gt;Sanity charges for hosted infrastructure: API requests, datasets, and seats. WordPress.org charges nothing for the software itself, but you pay for the server, database, CDN, backups, security plugins, and often a premium theme or page builder on top.&lt;/p&gt;

&lt;p&gt;The line people miss is that Sanity's pricing is usage-based and predictable once you model your traffic. WordPress.org costs scale with the number of plugins you depend on and the hosting tier you need to stay stable under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity pricing tiers in 2026
&lt;/h2&gt;

&lt;p&gt;Sanity's current public plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;: 1 project, 2 non-admin users, 500k API CDN requests/month, 20 GB bandwidth, 10 GB assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth&lt;/strong&gt;: $15/month (billed annually), 3 non-admin users included, then $10/user/month, 1M API CDN requests/month, 100 GB bandwidth, 50 GB assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team&lt;/strong&gt;: $99/month, 5 non-admin users, 5M API CDN requests/month, unlimited datasets, priority support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise&lt;/strong&gt;: custom — starts around $1,500/month based on public case studies; includes SLA, SSO, custom roles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What actually triggers an upgrade from Free to Growth: hitting the 500k CDN request ceiling or needing a third editor. For a content site doing ~30 posts/month and pulling data at build time plus client-side Sanity Live, 500k requests can go fast if you're not caching GROQ responses at the edge. Growth at $15/month is a low bar — most client projects land here by month two.&lt;/p&gt;

&lt;p&gt;Bandwidth overage on Growth is $0.10/GB above the 100 GB cap. Asset storage overage is $0.05/GB above 50 GB. Neither is punishing at small scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress.org cost breakdown
&lt;/h2&gt;

&lt;p&gt;This is where you have to be honest. A "free" WordPress site has real line items:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost category&lt;/th&gt;
&lt;th&gt;Budget tier ($/yr)&lt;/th&gt;
&lt;th&gt;Mid tier ($/yr)&lt;/th&gt;
&lt;th&gt;High-traffic tier ($/yr)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed hosting (e.g. Kinsta, WP Engine)&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;td&gt;$600–$1,200&lt;/td&gt;
&lt;td&gt;$3,000–$6,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium theme or page builder&lt;/td&gt;
&lt;td&gt;$60–$100&lt;/td&gt;
&lt;td&gt;$200–$400&lt;/td&gt;
&lt;td&gt;$400 (multi-site)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security plugin (Wordfence Pro, Solid Security Pro)&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;td&gt;$120–$250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup plugin (BlogVault, UpdraftPlus Plus)&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;td&gt;$120&lt;/td&gt;
&lt;td&gt;$120–$400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO plugin (Yoast SEO Premium, Rank Math Pro)&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$99–$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN add-on or Cloudflare Pro&lt;/td&gt;
&lt;td&gt;$0–$240&lt;/td&gt;
&lt;td&gt;$240&lt;/td&gt;
&lt;td&gt;$240–$600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance plugin (WP Rocket, Perfmatters)&lt;/td&gt;
&lt;td&gt;$59&lt;/td&gt;
&lt;td&gt;$59&lt;/td&gt;
&lt;td&gt;$59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated developer maintenance (hours × rate)&lt;/td&gt;
&lt;td&gt;$0 (DIY)&lt;/td&gt;
&lt;td&gt;$600–$1,200&lt;/td&gt;
&lt;td&gt;$2,000–$4,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Annual total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$760–$860&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2,040–$3,450&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6,040–$11,650&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mid-tier number is what a real business running WooCommerce or a high-traffic blog actually pays without ever opening an enterprise conversation. The developer maintenance line is the one most cost comparisons omit — WordPress security patches, plugin conflicts, and PHP updates happen on a schedule you don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity annual cost at equivalent scale
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;th&gt;Sanity plan&lt;/th&gt;
&lt;th&gt;Annual cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Side project or MVP&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small team, 2–4 editors&lt;/td&gt;
&lt;td&gt;Growth ($15/mo × 12 + 1 extra seat)&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency or content-heavy site&lt;/td&gt;
&lt;td&gt;Team ($99/mo × 12)&lt;/td&gt;
&lt;td&gt;$1,188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise / multi-tenant&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;$18,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add Next.js hosting (Vercel Pro at $240/yr for small teams, or a VPS at ~$120/yr) and you're at &lt;strong&gt;$420–$1,428/yr&lt;/strong&gt; for small-to-medium Sanity + Next.js projects. No security patching cycle. No plugin conflicts. No PHP. Deployments are static-first with ISR revalidation on Sanity webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where WordPress.org wins on cost
&lt;/h2&gt;

&lt;p&gt;A single developer who already knows WordPress and doesn't need Sanity's structured content model can stand up a site faster with fewer moving parts. If the project is a blog where one person writes and publishes, and you're already on shared hosting you've paid for, the marginal cost of WordPress.org is genuinely near zero.&lt;/p&gt;

&lt;p&gt;WooCommerce is also a meaningful advantage: Sanity doesn't have a native commerce layer. If you want a headless WooCommerce + Next.js setup you're adding API complexity on both ends.&lt;/p&gt;

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

&lt;p&gt;At the mid tier, Sanity's cost curve is flatter. Once you're paying $1,000–$1,200/yr for Sanity Team + Vercel Pro, you've eliminated the variable plugin/security/maintenance spend that makes WordPress.org unpredictable. No CDN configuration, no uptime monitoring setup, no emergency patches when a plugin author drops a vulnerability.&lt;/p&gt;

&lt;p&gt;For multi-locale sites or structured content that needs to be consumed by mobile apps, a CMS API, or multiple front-ends simultaneously, Sanity's GROQ API delivers what WordPress.org requires a full REST/GraphQL plugin stack to approximate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upgrade trigger comparison
&lt;/h2&gt;

&lt;p&gt;Sanity's upgrade is API requests and seats — both are predictable. If you hit the Growth CDN ceiling, you can switch to ISR with longer revalidation windows or add edge caching in front of Sanity's API CDN to stay on the free plan longer.&lt;/p&gt;

&lt;p&gt;WordPress.org's upgrade trigger is usually a combination of: a plugin going abandoned, a server running out of headroom during a traffic spike, or a PHP/MySQL version incompatibility after a host OS upgrade. Those are harder to model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to recommend
&lt;/h2&gt;

&lt;p&gt;If a client already runs WordPress, has a developer on staff who manages it, and doesn't need multi-channel structured content: stick with WordPress.org. The switching cost isn't justified.&lt;/p&gt;

&lt;p&gt;If a client is starting fresh, needs a clean editor experience, expects a Next.js front-end, and values predictable infra costs over time: Sanity at $300/yr for a small team beats mid-tier WordPress at $2,000–$3,000/yr once you count plugins and maintenance honestly.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Payload CMS vs Storyblok: Pricing Comparison 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:41:41 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-storyblok-pricing-comparison-2026-3p7m</link>
      <guid>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-storyblok-pricing-comparison-2026-3p7m</guid>
      <description>&lt;p&gt;Sanity, Payload CMS, and Storyblok are the three headless CMS platforms I quote most often when scoping Next.js projects in 2026. Pricing is almost always the first question, and the answer is genuinely different depending on team size, content volume, and whether you want managed hosting. This post compares real numbers — no estimates, no hand-waving.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each pricing model works
&lt;/h2&gt;

&lt;p&gt;Before the table, a quick structural note because the three models are fundamentally different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt; charges on the hosted API tier. The free plan (called Free) gives you one project, unlimited admin users, 10 GB asset storage, 500k API CDN requests per month, and a dataset cap of 500k documents. Overage pricing kicks in at the dataset and API request layer — you don't pay per editor seat on Free or Growth, which is a meaningful differentiator. Growth is $15/month (billed annually) per project and raises limits substantially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload CMS&lt;/strong&gt; is open-source software you self-host. The software itself is free under MIT. "Pricing" means your infrastructure bill — a Render or Railway instance, a MongoDB Atlas or Postgres cluster, and object storage for assets. Payload Cloud (managed hosting) launched in 2024 and was revised in 2025; as of 2026 it starts at $45/month for a single project. If you self-host well, Payload can be genuinely the cheapest option. If you are sloppy, the ops cost adds up fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storyblok&lt;/strong&gt; charges per seat. The free Community plan allows one user and up to 25k API calls per month. The Starter plan is $99/month for two seats. Growth jumps to $299/month and adds more seats, spaces, and webhook triggers. Enterprise is custom. The per-seat model means a five-person marketing team on Storyblok costs real money quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side pricing table
&lt;/h2&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;Sanity Free&lt;/th&gt;
&lt;th&gt;Sanity Growth&lt;/th&gt;
&lt;th&gt;Payload self-hosted&lt;/th&gt;
&lt;th&gt;Payload Cloud&lt;/th&gt;
&lt;th&gt;Storyblok Community&lt;/th&gt;
&lt;th&gt;Storyblok Starter&lt;/th&gt;
&lt;th&gt;Storyblok Growth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$15/project&lt;/td&gt;
&lt;td&gt;~$20–60 infra&lt;/td&gt;
&lt;td&gt;$45&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$299&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor seats&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API requests/month&lt;/td&gt;
&lt;td&gt;500k CDN&lt;/td&gt;
&lt;td&gt;10M CDN&lt;/td&gt;
&lt;td&gt;Self-managed&lt;/td&gt;
&lt;td&gt;Self-managed&lt;/td&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;250k&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset storage&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;td&gt;Self-managed&lt;/td&gt;
&lt;td&gt;20 GB&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;25 GB&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environments&lt;/td&gt;
&lt;td&gt;2 datasets&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1 space&lt;/td&gt;
&lt;td&gt;3 spaces&lt;/td&gt;
&lt;td&gt;5 spaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom roles / RBAC&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (custom)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrastructure estimates for Payload self-hosted: $7/month Railway Starter, $9/month MongoDB Atlas M10 (minimum for production), $5/month Cloudflare R2 for assets — roughly $21/month if you stay small. A more serious setup (dedicated DB, blob storage, preview deployments) runs $50–100/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually triggers an upgrade
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt; upgrades are triggered by API CDN requests, not seats — which makes it predictable for teams with many editors but low traffic. A marketing site with 200k monthly visitors, image-heavy pages, and ISR cache hits almost never breaches the Growth plan limits. Where you do get hit: real-time preview endpoints that bypass CDN, or a site with a lot of unauthenticated GROQ queries fired client-side. Sanity's pricing page shows overage rates at $0.50 per additional 100k API requests and $0.10 per extra GB of storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload Cloud&lt;/strong&gt; upgrades are straightforward — you move from the $45 Starter to custom enterprise pricing when you need more compute, more environments, or SLA guarantees. Self-hosted Payload scales differently: the trigger is your own comfort level with Mongo ops and keeping up with Payload releases, not a billing threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storyblok&lt;/strong&gt; upgrades are seat-triggered. One extra editor over the plan limit and you're on the next tier. The jump from Starter ($99) to Growth ($299) is $200/month — a significant step. Growth also unlocks granular roles (useful for separating content editors from devs) and higher API limits. If you're building a site that an agency will hand off to a five-person client team, budget for Growth from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total cost of ownership over 12 months
&lt;/h2&gt;

&lt;p&gt;Here's a realistic scenario: two developers, three marketing editors, a production site, and a staging environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sanity Growth&lt;/strong&gt;: $15/month × 12 = &lt;strong&gt;$180/year&lt;/strong&gt;. No seat cost. Staging uses a second dataset on the same project (within plan limits). Total: $180.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload self-hosted&lt;/strong&gt;: Railway + Atlas + R2 ≈ $50/month × 12 = &lt;strong&gt;$600/year&lt;/strong&gt;. Add dev time for ops — patching, backups, migration scripts. Real cost is $600 plus 4–8 hours of engineering time annually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload Cloud&lt;/strong&gt;: $45/month × 12 = &lt;strong&gt;$540/year&lt;/strong&gt;. No ops burden. Competitive with Sanity Growth for small teams if you value managed infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storyblok Growth&lt;/strong&gt;: $299/month × 12 = &lt;strong&gt;$3,588/year&lt;/strong&gt;. Three editors push you firmly into Growth. The seat model is punishing for content-heavy teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a solo founder or two-person team, Storyblok Community and Sanity Free are both usable. Past five people, Storyblok's costs compound fast. Sanity scales editors without charging for them; Payload gives you a zero-seat-cost self-hosted option if you have the ops maturity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which to pick based on budget constraints
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Under $50/month total&lt;/strong&gt;: Sanity Free plus Vercel hobby, or Payload self-hosted on Railway. Sanity wins on zero-config; Payload wins if you need SQL or custom auth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$50–$150/month&lt;/strong&gt;: Sanity Growth ($15/project) plus whatever hosting you already pay. Most real projects live here. Payload Cloud ($45) is the alternative if you prefer a unified managed service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$150–$400/month&lt;/strong&gt;: Storyblok Starter becomes relevant only if your team is two people and you specifically want the visual editor experience. Otherwise, Sanity or Payload still beat it on cost for equivalent functionality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over $400/month&lt;/strong&gt;: You're looking at Storyblok Growth or Sanity custom/enterprise. At this scale, seat count and environment limits matter more than per-unit costs — get quotes from each vendor directly.&lt;/p&gt;

&lt;p&gt;The short version: Sanity wins on predictable low cost for multi-editor teams. Payload wins on long-run cost if you're self-hosting and have ops capacity. Storyblok wins on editor UX, not pricing.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Payload CMS vs Contentful: Pricing Comparison 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:58:17 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-contentful-pricing-comparison-2026-2pf7</link>
      <guid>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-contentful-pricing-comparison-2026-2pf7</guid>
      <description>&lt;p&gt;Sanity, Payload CMS, and Contentful pricing in 2026 looks radically different depending on how you count. Sanity charges per seat plus usage, Payload is open-source but carries real infrastructure costs, and Contentful's jump from free to its first paid plan is steep. This post puts all three side by side with real numbers so you can make an informed decision before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these three get compared
&lt;/h2&gt;

&lt;p&gt;These are the three CMS options I see shortlisted most often for Next.js App Router projects. Sanity is the structured-content default for most greenfield work. Payload is the choice when teams want an open-source, self-hosted option with a proper TypeScript-native schema layer. Contentful is the incumbent — huge enterprise penetration, a recognisable brand, and pricing that surprises people the first time they see a quote.&lt;/p&gt;

&lt;p&gt;They serve similar use cases but land in completely different cost brackets depending on your team size, traffic, and whether you want to manage infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Sanity's Free plan gives you up to 20 seats, 2 public datasets, 10k documents, 1M API CDN requests and 250k API requests per month, 100 GB bandwidth, and 100 GB of assets. For a small marketing site or a developer experimenting, that holds for a surprisingly long time.&lt;/p&gt;

&lt;p&gt;The Growth plan is $15 per seat per month (Viewer seats are free), up to 50 seats. It adds private datasets and raises documents to 25k, with the same 1M API CDN / 250k API request quotas, and pay-as-you-go overages: $1 per extra 250k API CDN requests, $1 per extra 25k API requests, $0.30/GB bandwidth and $0.50/GB assets (sanity.io/pricing at the time of writing).&lt;/p&gt;

&lt;p&gt;Enterprise pricing is custom, with negotiated quotas, SLAs and dedicated support.&lt;/p&gt;

&lt;p&gt;Note: Growth is billed per seat, so cost scales with your editing team — but because the Free plan already allows 20 seats, many small teams never need to pay for seats at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payload CMS pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Payload itself is MIT-licensed and free. There is no per-seat cost, no API request limit tied to the CMS vendor, and no bandwidth charge from Payload. Your cost is entirely infrastructure.&lt;/p&gt;

&lt;p&gt;Payload Cloud, the managed hosting option, is not a realistic choice for new projects right now: after Payload joined Figma (June 2025), Payload announced that deployment of new Cloud projects is paused, with existing Cloud projects kept running. Check payloadcms.com for its current status. In practice, budget for self-hosting.&lt;/p&gt;

&lt;p&gt;If you self-host on Railway or Fly.io instead: expect $20–$60/month for a small project (shared Postgres instance + small container). Add managed Postgres from Neon or Supabase ($0–$25/month) and a CDN (Cloudflare free tier covers most cases). Total self-hosted cost for a small production project sits around $30–$80/month — you are on the hook for uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contentful pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Contentful's Free plan caps users, records, and monthly API calls, and the limits have changed several times — check contentful.com/pricing for the current numbers before you rely on it. It is genuinely limited for a production site with frequent revalidation.&lt;/p&gt;

&lt;p&gt;The entry paid plan (Lite) has been listed at around $300/month at the time of writing. That jump — from $0 to roughly $300 — is the "Contentful cliff" the community has complained about for years. Above that, Premium is custom-priced.&lt;/p&gt;

&lt;p&gt;Contentful's plans also cap spaces, environments, and locales, which matters for multi-brand or multi-region setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side pricing table
&lt;/h2&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;Sanity Free&lt;/th&gt;
&lt;th&gt;Sanity Growth&lt;/th&gt;
&lt;th&gt;Payload Self-Hosted&lt;/th&gt;
&lt;th&gt;Contentful Free&lt;/th&gt;
&lt;th&gt;Contentful Lite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$15/seat&lt;/td&gt;
&lt;td&gt;~$30–80 infra&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;~$300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor seats&lt;/td&gt;
&lt;td&gt;Up to 20&lt;/td&gt;
&lt;td&gt;Up to 50 (paid per seat; Viewers free)&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API requests&lt;/td&gt;
&lt;td&gt;1M CDN + 250k API/mo&lt;/td&gt;
&lt;td&gt;1M CDN + 250k API/mo, then pay-as-you-go&lt;/td&gt;
&lt;td&gt;No limit (vendor)&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;td&gt;Higher cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;10k&lt;/td&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;Depends on DB&lt;/td&gt;
&lt;td&gt;Capped records&lt;/td&gt;
&lt;td&gt;Capped records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset storage&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;td&gt;100 GB, then $0.50/GB&lt;/td&gt;
&lt;td&gt;Depends on host&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;td&gt;100 GB, then $0.30/GB&lt;/td&gt;
&lt;td&gt;Depends on host&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;td&gt;Capped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript schemas&lt;/td&gt;
&lt;td&gt;Yes (TS config + TypeGen)&lt;/td&gt;
&lt;td&gt;Yes (TS config + TypeGen)&lt;/td&gt;
&lt;td&gt;Yes (native TS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Contentful caps change often; check its pricing page for current numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually triggers an upgrade
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt;: The trigger is usually private datasets, the 10k document cap, or needing pay-as-you-go headroom on API requests — not users, since Free allows 20 seats. A Next.js site revalidating dozens of routes every few minutes can eat into the 250k uncached API requests faster than you expect, so prefer the API CDN and tag-based revalidation. Moving to Growth ($15/seat/month) is low friction — the bigger risk is not watching overage costs if traffic spikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt;: The trigger is infrastructure reliability, not the software. A minimal container and shared database will struggle under sustained traffic, and moving to properly sized hosting happens when you need uptime guarantees. With new Payload Cloud projects paused, teams without a DevOps person need a managed platform (Vercel plus a managed Postgres, for example) or someone to own the infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contentful&lt;/strong&gt;: The trigger is hitting the Free plan's user, record, or API-call caps, which happens fast. The roughly $300/month jump is brutal for small teams. If you are a 3-person team with a modest site, you will either stay artificially small on Free or swallow a significant cost jump. There is no middle tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one is cheaper at different scales
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solo developer / tiny site&lt;/strong&gt;: Sanity Free or Payload self-hosted. Contentful Free is too limited on API calls for a Next.js ISR setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5–10 editors, moderate traffic&lt;/strong&gt;: Sanity wins on cost. Free covers up to 20 seats if public datasets and 10k documents are enough; Growth at $15/seat/month (so $75–$150/month for 5–10 paid seats) still undercuts Contentful Lite. Payload self-hosted is cheaper on paper but costs DevOps time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15–30 editors, multi-locale&lt;/strong&gt;: Payload self-hosted if you want no seat costs and own your data. Sanity Growth costs $225–$450/month for 15–30 paid seats (Viewers free), so it is no longer automatically the cheapest. Contentful Lite (~$300/month) only makes sense if your team is deeply embedded in the Contentful workflow and unwilling to migrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise / multi-brand&lt;/strong&gt;: Contentful has the deepest enterprise tooling (Workflows, Tags, Organisations). The cost reflects that. Sanity Enterprise competes at similar price points. Payload Enterprise is an emerging option — worth evaluating if data residency or full schema control matters more than enterprise support SLAs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest verdict
&lt;/h2&gt;

&lt;p&gt;For most Next.js + Sanity shops reading this: Sanity Free, then Growth at $15/seat/month, is the easiest decision at small-to-medium scale. Payload makes sense when you need TypeScript-native schemas, full data ownership, and are comfortable managing infrastructure. Contentful's pricing model has not caught up with the market — the $0-to-$300 cliff is a legitimate reason to look elsewhere in 2026, not just a community gripe.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Payload CMS vs Directus: Pricing Comparison 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:37:55 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-directus-pricing-comparison-2026-482o</link>
      <guid>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-directus-pricing-comparison-2026-482o</guid>
      <description>&lt;p&gt;Pricing for headless CMS platforms is genuinely hard to compare because each charges on a different axis — seats, API calls, bandwidth, compute, or a mix. This post puts Sanity, Payload CMS, and Directus side by side with concrete numbers so you can make a real decision before you commit to one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this trio keeps coming up
&lt;/h2&gt;

&lt;p&gt;These three platforms attract similar buyers: teams building structured-content sites on Next.js who want a proper content model and a developer-friendly API. But they monetise completely differently. Sanity is SaaS-hosted with a generous free tier. Payload is self-hosted open-source with an optional Cloud offering. Directus is open-source too, but its Cloud plans are priced per project and per seat. That difference shapes the total cost of ownership more than any feature spreadsheet does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;th&gt;Payload CMS&lt;/th&gt;
&lt;th&gt;Directus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Free Plan: 2 non-admin users, 10 GB bandwidth, 20 GB assets, 3 projects&lt;/td&gt;
&lt;td&gt;Community (self-hosted): unlimited — you pay infra only&lt;/td&gt;
&lt;td&gt;Community (self-hosted): unlimited — you pay infra only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheapest paid SaaS tier&lt;/td&gt;
&lt;td&gt;Growth: $15/month per project (5 non-admin users, 50 GB bandwidth)&lt;/td&gt;
&lt;td&gt;Payload Cloud Starter: $19/month (1 project, 1 GB DB, 5 GB storage)&lt;/td&gt;
&lt;td&gt;Directus Cloud Starter: ~$15/month (1 project, 1 seat included)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-seat model&lt;/td&gt;
&lt;td&gt;Non-admin editors free on Growth; admin seats priced separately above 5&lt;/td&gt;
&lt;td&gt;No per-seat on Cloud — per-project pricing&lt;/td&gt;
&lt;td&gt;Yes: additional seats $7–10/seat/month depending on tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth overage&lt;/td&gt;
&lt;td&gt;$0.10/GB above plan limit&lt;/td&gt;
&lt;td&gt;Included up to plan limit; overage charged per GB&lt;/td&gt;
&lt;td&gt;$0.10/GB above plan; Starter caps at 50 GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset storage overage&lt;/td&gt;
&lt;td&gt;$0.10/GB above plan limit&lt;/td&gt;
&lt;td&gt;~$0.25/GB above plan limit&lt;/td&gt;
&lt;td&gt;$0.10/GB above plan limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted option&lt;/td&gt;
&lt;td&gt;No (Studio can be self-hosted; Content Lake is SaaS only)&lt;/td&gt;
&lt;td&gt;Yes — MIT licence, zero licence cost&lt;/td&gt;
&lt;td&gt;Yes — AGPL licence, zero licence cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise pricing&lt;/td&gt;
&lt;td&gt;Custom; typically starts north of $1,000/month&lt;/td&gt;
&lt;td&gt;Custom; contact sales&lt;/td&gt;
&lt;td&gt;Custom; Directus Enterprise starts around $2,000/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sanity pricing in practice
&lt;/h2&gt;

&lt;p&gt;Sanity's free plan covers a surprising amount: three projects, 20 GB asset storage, 10 GB monthly bandwidth, and two non-admin users per project. For a small marketing site with a one-person editorial team that's genuinely zero cost — assuming you stay under the bandwidth ceiling.&lt;/p&gt;

&lt;p&gt;The trigger for upgrading to Growth ($15/project/month) is almost always one of two things: you exceed the bandwidth cap, or you need more than two non-admin editor seats. Growth gives you 50 GB bandwidth and five non-admin users per project. Overage beyond that is $0.10/GB on bandwidth and $0.10/GB on assets — predictable enough that you can budget it.&lt;/p&gt;

&lt;p&gt;The ceiling Sanity buyers hit most is the Content Lake API rate limits on the free plan (roughly 1,000 API CDN requests per second; unpublished draft API calls are much lower). High-traffic news sites hit this before they hit bandwidth. Growth raises those limits meaningfully.&lt;/p&gt;

&lt;p&gt;For a Next.js site with ISR revalidation and a Sanity webhook setup, the Growth plan at $15/month is almost always sufficient until you're well past 50,000 monthly visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payload CMS pricing in practice
&lt;/h2&gt;

&lt;p&gt;Payload's core value proposition is that the Community edition is free forever, self-hosted, MIT-licensed, and production-ready. You pay infrastructure — a PostgreSQL database, a file storage bucket (S3 or R2), and a compute host. A minimal Railway or Render setup runs $5–15/month. Add S3 or Cloudflare R2 for storage and you're typically under $20/month total for low-to-medium traffic.&lt;/p&gt;

&lt;p&gt;Payload Cloud is the managed option: Starter at $19/month gives you one project with 1 GB database, 5 GB storage, and a managed deploy. The Pro plan ($49/month) moves to 10 GB DB and 50 GB storage. These are competitive numbers but the sweet spot for Payload is still self-hosted — the Cloud product is relatively new and the community tooling around self-deployment is mature.&lt;/p&gt;

&lt;p&gt;Where Payload gets expensive is engineering time. Setting up CI/CD, database backups, storage integrations, and zero-downtime deploys requires more ops work than Sanity or Directus Cloud. For a solo freelancer or a startup without DevOps experience, that hidden cost is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directus pricing in practice
&lt;/h2&gt;

&lt;p&gt;Directus splits the same way: self-hosted Community (AGPL, free) vs Directus Cloud. The AGPL licence means if you modify Directus and expose it as a service, you must open-source those modifications — that's a dealbreaker for some SaaS products but irrelevant for most content-site use cases.&lt;/p&gt;

&lt;p&gt;Directus Cloud Starter is around $15/month for one project and one included seat. Additional editors cost $7–10/seat/month, which is the most common cost complaint. A 10-person editorial team would pay $15 + (9 × $8) = $87/month minimum on Starter — before hitting bandwidth or storage limits. That adds up faster than Sanity for editor-heavy newsroom setups.&lt;/p&gt;

&lt;p&gt;The self-hosted route avoids per-seat costs entirely. A DigitalOcean or Railway instance running Directus with a managed PostgreSQL database costs roughly $10–25/month, depending on traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total cost comparison at three scale points
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;th&gt;Payload (self-hosted)&lt;/th&gt;
&lt;th&gt;Directus (self-hosted)&lt;/th&gt;
&lt;th&gt;Directus Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo dev / prototype&lt;/td&gt;
&lt;td&gt;$0 (free plan)&lt;/td&gt;
&lt;td&gt;~$10/month (infra)&lt;/td&gt;
&lt;td&gt;~$10/month (infra)&lt;/td&gt;
&lt;td&gt;$15/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-editor marketing site&lt;/td&gt;
&lt;td&gt;$15/month (Growth)&lt;/td&gt;
&lt;td&gt;~$20/month (infra + DB)&lt;/td&gt;
&lt;td&gt;~$10–20/month (infra)&lt;/td&gt;
&lt;td&gt;$47/month (1 + 4 seats)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-editor content team&lt;/td&gt;
&lt;td&gt;$15–30/month + overages&lt;/td&gt;
&lt;td&gt;~$25–40/month (infra)&lt;/td&gt;
&lt;td&gt;~$20–30/month (infra)&lt;/td&gt;
&lt;td&gt;$150+/month (per seat)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one to pick on cost alone
&lt;/h2&gt;

&lt;p&gt;If you want zero infrastructure management and have a small editorial team, Sanity's free plan or $15/month Growth tier is genuinely hard to beat. The Content Lake handles delivery, the CDN is included, and you don't touch a database.&lt;/p&gt;

&lt;p&gt;If your team is larger than five editors or you want no per-seat charges and are comfortable with self-hosting, Payload self-hosted is the cheapest path at scale — MIT licence, low infra cost, and no surprises.&lt;/p&gt;

&lt;p&gt;Directus Cloud becomes expensive quickly once you add editors. The self-hosted Community version is cost-competitive with Payload, but the AGPL licence needs a legal read before you deploy commercially.&lt;/p&gt;

&lt;p&gt;The real cost inflection points: Sanity triggers at bandwidth; Payload at engineering time; Directus Cloud at seat count. Pick the one whose constraint you're furthest from.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Payload CMS vs Prismic: Pricing Comparison 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:05:02 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-prismic-pricing-comparison-2026-51fc</link>
      <guid>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-prismic-pricing-comparison-2026-51fc</guid>
      <description>&lt;p&gt;Sanity, Payload CMS, and Prismic are three of the most searched headless CMS options for Next.js teams in 2026, and their pricing models are structured so differently that a direct comparison is harder than it looks. This post cuts through the marketing pages and maps out what each platform actually costs at three realistic scales: a solo side project, a small product team, and an agency running multiple client sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these three are hard to compare directly
&lt;/h2&gt;

&lt;p&gt;Payload is self-hosted open source by default — no per-seat fee, no usage meter, but you're paying for compute and Postgres somewhere. Sanity is a managed API service with a generous free tier and a growth plan that charges on seats and dataset usage. Prismic is per-seat SaaS with a document limit twist that catches people off guard. They're solving the same problem in three different commercial structures, which makes apples-to-apples pricing non-trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Sanity's free tier (called Free) gives you one project, three non-admin users, 500k API requests per month, 10 GB bandwidth, and 20 GB of storage assets. It covers most portfolio sites and small blogs indefinitely.&lt;/p&gt;

&lt;p&gt;The Growth plan starts at $15/month per seat (minimum one seat). That seat count is what triggers most upgrades — once a content team has more than three editors, Growth is required. On Growth you get 500k API requests included and then $8 per additional 100k, plus $1/GB beyond 100 GB bandwidth. Dataset limits are relaxed significantly.&lt;/p&gt;

&lt;p&gt;Enterprise pricing is custom but starts around $3,000/month in practice for teams that need SSO, audit logs, and dedicated support SLAs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payload CMS pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Payload's open-source version is free forever and has no seat restrictions. You run it on your own infrastructure — a $10–20/month Fly.io or Railway instance plus a Postgres database gets you production-ready. The only real cost is your time to maintain it.&lt;/p&gt;

&lt;p&gt;Payload Cloud (the managed offering) launched post-2.0 and is positioned as a zero-ops path. Pricing runs roughly $25/month for the starter tier (1 project, 1 GB DB, 10 GB media storage), $75–100/month for a team tier with more storage and environments. These numbers have shifted in 2026 as Payload Cloud matures — check their site for current rates, but the floor is meaningfully higher than DIY infrastructure.&lt;/p&gt;

&lt;p&gt;The ceiling difference matters: self-hosting Payload at scale is predictable cost-wise (you're paying cloud infrastructure rates), while Payload Cloud adds a margin on top. For agencies running 10+ client sites, self-hosted Payload on a shared VPS cluster is frequently the cheapest option across all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prismic pricing in 2026
&lt;/h2&gt;

&lt;p&gt;Prismic uses a per-seat model with a document count ceiling that surprises teams after year one. The free tier allows 1 user, 1 repository, and 1,000 documents (including every locale variant separately). That document limit is the real constraint — an e-commerce site with 500 products in two languages has already hit it.&lt;/p&gt;

&lt;p&gt;The Starter plan is $15/seat/month with up to 10,000 documents and 3 repositories. The Professional tier runs $30/seat/month, bumps document limits to 50,000, and adds more environments and custom types. At five editors on Starter, you're paying $75/month before bandwidth or API overages.&lt;/p&gt;

&lt;p&gt;Prismic's bandwidth pricing is not public-facing — they bundle it into plan tier rather than charging per GB. In practice this means you can't scale-test the true ceiling before committing to a higher tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side cost table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;th&gt;Payload CMS&lt;/th&gt;
&lt;th&gt;Prismic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo project / portfolio&lt;/td&gt;
&lt;td&gt;Free forever&lt;/td&gt;
&lt;td&gt;Free (self-host ~$10/mo infra)&lt;/td&gt;
&lt;td&gt;Free (1 user, 1k docs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 editors, 1 site&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Free + ~$15/mo infra&lt;/td&gt;
&lt;td&gt;$45/mo (3 seats Starter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 editors, 1 site&lt;/td&gt;
&lt;td&gt;$75/mo (5 seats Growth)&lt;/td&gt;
&lt;td&gt;Free + ~$20/mo infra OR ~$75/mo Cloud&lt;/td&gt;
&lt;td&gt;$75/mo (5 seats Starter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 editors, 5 client sites&lt;/td&gt;
&lt;td&gt;~$75/mo + per-project add-ons&lt;/td&gt;
&lt;td&gt;Self-host ~$30–50/mo shared&lt;/td&gt;
&lt;td&gt;$150/mo + extra repositories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise (20+ editors)&lt;/td&gt;
&lt;td&gt;Custom (~$3k+/mo)&lt;/td&gt;
&lt;td&gt;Self-host at scale / Cloud custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Payload's self-hosted model wins on pure cost at nearly every scale. Sanity wins on zero-ops convenience and GROQ query flexibility. Prismic's per-seat cost converges with Sanity's Growth plan but the document limit adds a hidden upgrade trigger Sanity doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually triggers an upgrade
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt; — the trigger is almost always seat count, not API requests. Three content editors on a marketing team is the inflection point. Bandwidth overages are rare for sites under 50k monthly visitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt; — on the open-source path, the trigger is ops overhead: backups, migrations, Postgres maintenance. When a client starts asking about uptime SLAs, you either pay for Payload Cloud or you productionise your own infra more seriously. On Payload Cloud, storage is the first ceiling you hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prismic&lt;/strong&gt; — document count is the stealth trigger. Multi-locale setups and large product catalogues hit the free and Starter limits faster than teams expect. The second trigger is repository count once you're running sites for multiple clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which makes sense at which scale
&lt;/h2&gt;

&lt;p&gt;For a solo Next.js developer building client sites, Sanity Free or a self-hosted Payload instance covers 80% of projects with minimal cost. Prismic's free tier is too restrictive on documents to be useful for real client work.&lt;/p&gt;

&lt;p&gt;For a three-to-five person product team that wants a managed API and doesn't want to think about infrastructure, Sanity Growth is the cleanest path. The $75/month for five seats is predictable and the GROQ + Sanity Studio combination has the best developer experience of the three.&lt;/p&gt;

&lt;p&gt;For an agency running ten or more independent client sites, self-hosted Payload on a shared cluster is typically the cheapest option by a wide margin. The open-source license has no per-site fee, and a well-configured Railway or Fly.io setup costs less per month than two Prismic seats.&lt;/p&gt;

&lt;p&gt;For teams that need Slice Machine and a visual page builder that non-technical editors can own completely, Prismic is still the most polished option despite the seat cost — just budget for the document ceiling before you hit it in production.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Payload CMS vs Keystatic: Pricing Comparison 2026</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 30 Sep 2026 10:37:29 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-keystatic-pricing-comparison-2026-2l9a</link>
      <guid>https://dev.to/nayankyada/sanity-vs-payload-cms-vs-keystatic-pricing-comparison-2026-2l9a</guid>
      <description>&lt;p&gt;Choosing between Sanity, Payload CMS, and Keystatic almost always comes down to pricing at some point in the conversation. All three have free paths to production, but the cost curves diverge sharply once you add seats, environments, or serious traffic. Here is what you actually pay at each tier in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this comparison matters
&lt;/h2&gt;

&lt;p&gt;Sanity is API-first, fully hosted, and charges by usage and seats. Payload is open-source and self-hosted by default, shifting cost to infrastructure. Keystatic is Git-based and pushes most cost to your Git host and optional Keystatic Cloud. They solve overlapping problems — structured content for Next.js apps — but they are not interchangeable once you account for team size, deployment complexity, and who pays the ops bill.&lt;/p&gt;

&lt;p&gt;I've scoped projects on all three in the last twelve months. The sticker price rarely tells the full story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free tier limits side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Sanity Free&lt;/th&gt;
&lt;th&gt;Payload Free (self-hosted)&lt;/th&gt;
&lt;th&gt;Keystatic Free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hosted by vendor&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional (Cloud)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datasets / projects&lt;/td&gt;
&lt;td&gt;2 projects&lt;/td&gt;
&lt;td&gt;Unlimited (self-host)&lt;/td&gt;
&lt;td&gt;Unlimited repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users / editors&lt;/td&gt;
&lt;td&gt;3 non-admin users&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API requests&lt;/td&gt;
&lt;td&gt;250k CDN req/month&lt;/td&gt;
&lt;td&gt;Unlimited (your server)&lt;/td&gt;
&lt;td&gt;Git API limits apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assets / storage&lt;/td&gt;
&lt;td&gt;5 GB assets&lt;/td&gt;
&lt;td&gt;Your own storage&lt;/td&gt;
&lt;td&gt;Git LFS or external&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GROQ / query API&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;REST + GraphQL included&lt;/td&gt;
&lt;td&gt;N/A (Git reads)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environments (staging)&lt;/td&gt;
&lt;td&gt;1 (single dataset)&lt;/td&gt;
&lt;td&gt;Separate deploys&lt;/td&gt;
&lt;td&gt;Branch-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity Studio hosting&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Self-host&lt;/td&gt;
&lt;td&gt;Self-host&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sanity's free tier is genuinely useful for a single small project. Three non-admin users covers most founder-plus-one-editor setups. The 250k CDN request ceiling sounds large but a Next.js ISR site with moderate traffic and preview calls can brush against it faster than expected — especially once you add live preview sessions.&lt;/p&gt;

&lt;p&gt;Payload's free tier is unlimited in every meaningful sense because you own the server. But "free" here means you are running a Node.js process somewhere. A Railway Hobby plan ($5/month), a Render free-tier instance (with cold starts), or a VPS at $6/month from Hetzner — that's your baseline, not zero.&lt;/p&gt;

&lt;p&gt;Keystatic's free tier is genuinely zero if you stay on the open-source path and store content in your Git repo. GitHub or GitLab is the database. For most small sites, this is fine. For large content libraries (hundreds of documents with media), Git history bloat becomes a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paid tier thresholds
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;th&gt;Payload Cloud&lt;/th&gt;
&lt;th&gt;Keystatic Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entry paid&lt;/td&gt;
&lt;td&gt;$15/month (Growth)&lt;/td&gt;
&lt;td&gt;$25/month (Starter)&lt;/td&gt;
&lt;td&gt;$19/month (Team)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What triggers upgrade&lt;/td&gt;
&lt;td&gt;&amp;gt;3 non-admin users, &amp;gt;250k CDN req, &amp;gt;5 GB assets&lt;/td&gt;
&lt;td&gt;Wanting managed hosting&lt;/td&gt;
&lt;td&gt;Wanting managed hosting beyond GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-seat cost&lt;/td&gt;
&lt;td&gt;$15/seat above included&lt;/td&gt;
&lt;td&gt;Flat project pricing&lt;/td&gt;
&lt;td&gt;Flat team pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging environment&lt;/td&gt;
&lt;td&gt;$15/month extra dataset&lt;/td&gt;
&lt;td&gt;Separate project deploy&lt;/td&gt;
&lt;td&gt;Branch-based (free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API rate limits&lt;/td&gt;
&lt;td&gt;Generous on Growth&lt;/td&gt;
&lt;td&gt;Your server limits&lt;/td&gt;
&lt;td&gt;Git API limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom (typically $1k+/month)&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Not yet a clear path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sanity's growth plan at $15/month per seat is the most common pain point I hear about. A five-person editorial team — three editors, one PM, one developer — sits at $45–60/month before you've counted bandwidth or asset storage overages. Add a staging dataset and you're at $75/month. That's not outrageous for a professional content operation, but it surprises teams coming from self-hosted WordPress.&lt;/p&gt;

&lt;p&gt;Payload Cloud launched properly in 2024 and has matured. The Starter tier at $25/month gives you a managed MongoDB Atlas instance, file storage, and a hosted admin UI. It removes the ops burden without the per-seat cost structure. If your team is four developers with no non-technical editors, Payload Cloud is often cheaper than Sanity Growth.&lt;/p&gt;

&lt;p&gt;Keystatic Cloud is the newest entrant. At $19/month for teams it's competitive, but Keystatic's architecture means it only makes sense for content that lives happily in a Git repo — flat Markdown or YAML files, reasonable document counts, no heavy asset management. For a developer blog or docs site it's excellent. For a marketing team managing hundreds of landing pages with hotspot images and scheduled publishing, it's the wrong tool regardless of price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted cost reality for Payload
&lt;/h2&gt;

&lt;p&gt;Payload's self-hosted path deserves its own accounting. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute&lt;/strong&gt;: Railway Pro ($20/month), Render Starter ($7/month), or a $6 Hetzner VPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: MongoDB Atlas free tier covers small projects; M10 cluster (production-appropriate) starts at ~$57/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File storage&lt;/strong&gt;: AWS S3 free tier for the first year, then ~$0.023/GB/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups, monitoring&lt;/strong&gt;: optional but real costs in any serious deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A realistic production Payload setup runs $30–100/month before you hire anyone to maintain it. That's still competitive with Sanity Growth for large teams, but it's not free infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total cost of ownership at three scales
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team size&lt;/th&gt;
&lt;th&gt;Sanity&lt;/th&gt;
&lt;th&gt;Payload (self-hosted)&lt;/th&gt;
&lt;th&gt;Payload Cloud&lt;/th&gt;
&lt;th&gt;Keystatic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo dev, personal project&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;~$6–12/month&lt;/td&gt;
&lt;td&gt;$25/month&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-person startup (2 editors)&lt;/td&gt;
&lt;td&gt;$0–15/month&lt;/td&gt;
&lt;td&gt;~$30–60/month&lt;/td&gt;
&lt;td&gt;$25/month&lt;/td&gt;
&lt;td&gt;$0–19/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-person team (5 editors)&lt;/td&gt;
&lt;td&gt;~$75/month&lt;/td&gt;
&lt;td&gt;~$60–100/month&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;$19/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20+ editors, multi-locale&lt;/td&gt;
&lt;td&gt;$200+/month&lt;/td&gt;
&lt;td&gt;$100–150/month infra&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Not recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one to pick based on budget
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanity&lt;/strong&gt; wins on editor experience and GROQ flexibility. The cost is predictable once you've settled your seat count. For teams where non-technical editors are the majority of users, the per-seat model is worth it — the Studio quality keeps support tickets low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt; wins on total cost for developer-heavy teams or when you need full control of your data layer. Self-hosting has a real ops cost, but if you're already running infrastructure (many product teams are), adding Payload is marginal. Payload Cloud closes the ops gap at a flat rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keystatic&lt;/strong&gt; wins for content-in-Git workflows — docs sites, developer blogs, small marketing sites where a developer is close to the content. The pricing is lowest, the architecture is simplest, and branch-based staging is built in for free. It is the wrong choice when editors outnumber developers or when content is too large for a Git repo.&lt;/p&gt;

&lt;p&gt;None of these is universally cheapest. The gap between them matters most at the 5–20 editor range, where Sanity's per-seat cost accumulates and Payload's self-hosting complexity starts to feel justified.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Sanity vs Webflow Pricing 2026: Real Costs Side by Side</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 29 Sep 2026 10:49:32 +0000</pubDate>
      <link>https://dev.to/nayankyada/sanity-vs-webflow-pricing-2026-real-costs-side-by-side-5487</link>
      <guid>https://dev.to/nayankyada/sanity-vs-webflow-pricing-2026-real-costs-side-by-side-5487</guid>
      <description>&lt;p&gt;Sanity and Webflow both have "free" plans, but the cost curve diverges fast once you add editors, traffic, or a custom front end. This post puts the actual 2026 numbers side by side so you can see where each platform starts charging and what triggers the next tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually paying for on each platform
&lt;/h2&gt;

&lt;p&gt;Sanity is a headless CMS: you pay for the API, the hosted Studio, and asset delivery. Webflow bundles CMS, hosting, CDN, and visual editor into one product. That bundling makes price comparisons awkward — with Sanity you always need a separate host (Vercel, Netlify, Railway), whereas Webflow's hosting is included.&lt;/p&gt;

&lt;p&gt;To make this fair I'll break out the Sanity + Vercel cost as a single number where it matters, and I'll note when Webflow's bundled hosting is genuinely cheaper vs when it's padding the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free tier comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Sanity (Free)&lt;/th&gt;
&lt;th&gt;Webflow (Starter)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Projects&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seats / editors&lt;/td&gt;
&lt;td&gt;Up to 3&lt;/td&gt;
&lt;td&gt;1 (owner only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;10,000 per project&lt;/td&gt;
&lt;td&gt;50 CMS items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API requests&lt;/td&gt;
&lt;td&gt;250,000 / month&lt;/td&gt;
&lt;td&gt;N/A (no API access on free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset bandwidth&lt;/td&gt;
&lt;td&gt;10 GB CDN / month&lt;/td&gt;
&lt;td&gt;Not published (Webflow hosting required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domain&lt;/td&gt;
&lt;td&gt;Bring your own host&lt;/td&gt;
&lt;td&gt;Paid plan required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API / webhooks&lt;/td&gt;
&lt;td&gt;Yes, on free&lt;/td&gt;
&lt;td&gt;Webhooks require Basic or above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging / preview&lt;/td&gt;
&lt;td&gt;Draft mode, free&lt;/td&gt;
&lt;td&gt;Staging link, free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sanity's free tier is usable in production for small sites — you get real API access, webhooks, and three seats. Webflow's Starter is essentially a portfolio sandbox: 50 CMS items and no custom domain mean it doesn't survive contact with a real client project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paid plan pricing (September 2026)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sanity
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly (billed annually)&lt;/th&gt;
&lt;th&gt;Seats included&lt;/th&gt;
&lt;th&gt;Documents&lt;/th&gt;
&lt;th&gt;API calls&lt;/th&gt;
&lt;th&gt;Asset bandwidth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;250k&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth&lt;/td&gt;
&lt;td&gt;$15 / seat&lt;/td&gt;
&lt;td&gt;3 included&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team (volume)&lt;/td&gt;
&lt;td&gt;~$50–80 / seat&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Growth pricing is per seat above the three included. A five-person team pays $15 × 2 extra seats = $30/month on top of the $45 base, so $75/month total. Bandwidth overages on Growth are $0.10/GB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webflow
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly (billed annually)&lt;/th&gt;
&lt;th&gt;Editors&lt;/th&gt;
&lt;th&gt;CMS items&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;E-commerce&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;$14&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (static only)&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS&lt;/td&gt;
&lt;td&gt;$23&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;200 GB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;$39&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;400 GB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Webflow workspace seats (for designers) are priced separately from site editors. A Workspace Freelancer plan is $24/month and covers one person across unlimited sites. Workspace Team is $42/seat/month. If you have a team of designers &lt;em&gt;and&lt;/em&gt; editors, both meters run simultaneously — a common shock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each platform gets expensive
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanity cost cliffs:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extra seats on Growth.&lt;/strong&gt; At $15/seat, a ten-person content team costs $105/month in seat fees alone before bandwidth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset bandwidth.&lt;/strong&gt; Serving full-resolution Sanity CDN images without a transform pipeline burns the 10 GB free allowance in days on a mid-traffic blog. Use &lt;code&gt;?auto=format&amp;amp;w=800&lt;/code&gt; query params and cache at the edge to stay inside the free tier longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom roles.&lt;/strong&gt; Fine-grained document-level permissions (RBAC) require the Growth plan or above. If an agency needs to restrict editors to specific document types, that's a forcing function to paid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Webflow cost cliffs:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CMS item cap.&lt;/strong&gt; The 2,000-item CMS plan at $23/month caps out quickly for e-commerce catalogues or news archives. Moving to Business ($39) for 10,000 items is the first real jump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace seats for designers.&lt;/strong&gt; The per-seat workspace billing is separate from per-site billing. An agency with four designers + one site = $168/month in workspace fees before even counting the site plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No headless API on lower tiers.&lt;/strong&gt; Webflow's REST API and GraphQL access require at least the CMS site plan. If you want Next.js to fetch Webflow data, you're already at $23/month per site, and you still pay for a host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce rates.&lt;/strong&gt; Webflow takes a 2% transaction fee on the Standard e-commerce plan ($29/month). That disappears on Plus ($74) — a forced upgrade for any serious store.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sanity + Vercel vs Webflow all-in
&lt;/h2&gt;

&lt;p&gt;For a typical marketing site with 3 editors, moderate traffic (50k visits/month), and Next.js as the front end:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Sanity + Vercel&lt;/th&gt;
&lt;th&gt;Webflow (standalone)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CMS cost&lt;/td&gt;
&lt;td&gt;$45/month (Growth, 3 seats)&lt;/td&gt;
&lt;td&gt;$23/month (CMS plan)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;$20/month (Vercel Pro)&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Designer workspace&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;$24/month (Freelancer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$65/month&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$47/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Webflow wins on raw cost at this scale — but only if you don't need a custom front end. The moment you want Next.js handling rendering (RSC, ISR, PPR, custom components), you're fetching from Webflow's API anyway and still paying Vercel on top. At that point Webflow is just an expensive CMS with a visual editor you're no longer using for production layout.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;You have a custom Next.js front end and don't need Webflow's visual canvas.&lt;/li&gt;
&lt;li&gt;Your content model is complex: nested references, localized fields, block-level components. Webflow's CMS is flat; Sanity's is not.&lt;/li&gt;
&lt;li&gt;You need more than 10,000 CMS items. Sanity's Growth tier gives 100,000; Webflow Business tops out at 10,000 before enterprise.&lt;/li&gt;
&lt;li&gt;You want GROQ-powered queries with join-level control over payload size. Webflow's API returns fixed shapes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Webflow is cheaper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Marketing teams want to own layout and CMS in one tool, with no engineering dependency for design changes.&lt;/li&gt;
&lt;li&gt;The site is primarily landing pages with light CMS use (&amp;lt; 2,000 items).&lt;/li&gt;
&lt;li&gt;Budget is tight and a separate host is an extra line item nobody wants to justify.&lt;/li&gt;
&lt;li&gt;Visual editor parity matters — Webflow's Lottie/animation/CMS binding workflow has no Sanity equivalent out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number that usually decides it
&lt;/h2&gt;

&lt;p&gt;If the front end is already Next.js, Sanity almost always wins on total cost — the Webflow visual editor becomes sunk cost the moment a developer is rendering templates in RSC. If the site is entirely Webflow-rendered and the team is non-technical, Webflow's all-in pricing is genuinely simpler. The mistake is paying for Webflow's hosting &lt;em&gt;and&lt;/em&gt; Vercel because nobody committed to a rendering strategy at the start of the project.&lt;/p&gt;

</description>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
  </channel>
</rss>
