<?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>Directus Pricing 2026: Self-Hosted Costs, Cloud Tiers &amp; License Thresholds</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 14 Aug 2026 05:52:26 +0000</pubDate>
      <link>https://dev.to/nayankyada/directus-pricing-2026-self-hosted-costs-cloud-tiers-license-thresholds-26pa</link>
      <guid>https://dev.to/nayankyada/directus-pricing-2026-self-hosted-costs-cloud-tiers-license-thresholds-26pa</guid>
      <description>&lt;p&gt;Directus pricing is more nuanced than the homepage makes it look. The software itself is MIT-licensed and free to download, but "free" collapses fast once you account for hosting, managed database, storage, and — if your company clears a revenue threshold — a required sponsorship. Here is what it actually costs in 2026 across both deployment paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "free and open source" actually means for Directus
&lt;/h2&gt;

&lt;p&gt;Directus is genuinely open source under the Business Source License (BSL) 1.1, which converts to MIT after four years. For most teams — startups, agencies, indie products — there are no royalties and no per-seat fees. You clone the repo, wire up a Postgres or MySQL database, and run it yourself. The code costs nothing.&lt;/p&gt;

&lt;p&gt;The catch is that Directus is not a lightweight tool. At minimum you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Node.js server (the Directus process itself, roughly 512 MB RAM at idle, 1 GB comfortable)&lt;/li&gt;
&lt;li&gt;A managed relational database (Postgres is the practical default)&lt;/li&gt;
&lt;li&gt;Object storage for assets (S3-compatible)&lt;/li&gt;
&lt;li&gt;A reverse proxy or CDN in front&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That infrastructure is not free, and it does not manage itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted: realistic infrastructure costs
&lt;/h2&gt;

&lt;p&gt;Assuming you deploy on a mid-tier cloud provider (Railway, Render, Fly.io, or a VPS like Hetzner or DigitalOcean):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small project&lt;/strong&gt; (one editor, low traffic, &amp;lt; 5 GB assets):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directus on a 1 GB RAM instance: ~$7–12/month (Hetzner CX21, Fly.io shared, Render Starter)&lt;/li&gt;
&lt;li&gt;Managed Postgres (Railway Hobby or Supabase free tier → Pro at $25/month): $0–25/month&lt;/li&gt;
&lt;li&gt;S3 or Cloudflare R2 (&amp;lt; 10 GB stored): $0–2/month&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $7–39/month&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mid-size project&lt;/strong&gt; (5–15 editors, media-heavy, production SLA needed):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directus on 2–4 GB RAM instance: $20–50/month&lt;/li&gt;
&lt;li&gt;Managed Postgres with daily backups: $25–50/month (Supabase Pro, Neon, or Railway Pro)&lt;/li&gt;
&lt;li&gt;S3/R2 with egress at scale: $5–20/month&lt;/li&gt;
&lt;li&gt;Monitoring (Uptime Robot, Better Uptime): $0–20/month&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $50–140/month&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Larger project&lt;/strong&gt; (multi-tenant, HA, separate read replica):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directus with horizontal scaling or PM2 cluster: $80–200/month&lt;/li&gt;
&lt;li&gt;Managed Postgres with replication: $100–300/month (PlanetScale, RDS, Neon Business)&lt;/li&gt;
&lt;li&gt;CDN + egress: $20–100/month&lt;/li&gt;
&lt;li&gt;DevOps time for upgrades, migrations, backups: hard to price, but real&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $200–600/month before devops labour&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upgrades deserve a mention. Directus ships breaking changes between minor versions more aggressively than most CMSs. Keeping a self-hosted instance current is a quarterly task, not a one-time setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directus Cloud tiers (2026)
&lt;/h2&gt;

&lt;p&gt;Directus Cloud is the managed SaaS path. Directus manages the infrastructure; you get a Studio URL and API endpoint. Pricing as of mid-2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Monthly price&lt;/th&gt;
&lt;th&gt;Projects&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Asset storage&lt;/th&gt;
&lt;th&gt;API requests&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;$15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;500 k/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;td&gt;5 M/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Professional&lt;/td&gt;
&lt;td&gt;$299&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;200 GB&lt;/td&gt;
&lt;td&gt;20 M/month&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;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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Starter at $15/month is genuinely useful for a solo developer or a small marketing site. The 500 k API request ceiling sounds generous until you factor in Next.js ISR hitting the API on every revalidation cycle — a busy site can breach it in days. Standard at $99/month is where most real product teams land.&lt;/p&gt;

&lt;p&gt;Enterprise pricing is negotiated directly. Directus does not publish a floor publicly, but anecdotally expect a five-figure annual contract for dedicated infrastructure, SLA guarantees, and SSO.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sponsorship threshold
&lt;/h2&gt;

&lt;p&gt;This is the part that catches companies off guard. Directus's BSL license requires organisations generating more than &lt;strong&gt;$5 million USD in annual revenue&lt;/strong&gt; to become a paying sponsor to use the self-hosted version in production. The sponsorship starts at &lt;strong&gt;$1,500/year&lt;/strong&gt; (the "Silver" tier on GitHub Sponsors or the Directus website).&lt;/p&gt;

&lt;p&gt;This is not enforcement via code — there is no license key check. It is a legal obligation in the BSL. For a $5M+ company, $1,500/year is trivial, but teams should account for it in procurement, especially if legal or finance review vendor contracts.&lt;/p&gt;

&lt;p&gt;For sub-$5M companies: you owe nothing extra. For companies on the threshold evaluating Directus as part of a longer-term roadmap, the sponsorship should be in the budget from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Directus Cloud does not include
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom domains on Starter&lt;/strong&gt;: only on Standard and above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private networking / VPC&lt;/strong&gt;: Enterprise only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs&lt;/strong&gt;: not available on Starter tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based access control at the field level&lt;/strong&gt;: available across tiers, but complex RBAC configurations need hands-on setup time regardless&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email delivery&lt;/strong&gt;: Directus Cloud includes transactional email for auth flows but not bulk or custom sending — that is your problem (SendGrid, Resend, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-hosted vs Cloud: which cost path makes sense
&lt;/h2&gt;

&lt;p&gt;Cloud wins if your team does not have a dedicated DevOps resource, you want predictable pricing with no upgrade surprises, and your project fits inside Standard or Professional. At $99/month, Standard is cheaper than most mid-size self-hosted setups once you honestly price DevOps time.&lt;/p&gt;

&lt;p&gt;Self-hosted wins if you need to keep all data on your own infrastructure (GDPR, HIPAA, internal policy), you are comfortable maintaining Node.js services and Postgres at the OS level, and you have an existing cloud bill that can absorb the extra workload cheaply — a Hetzner server at $12/month running Directus alongside other services, for example.&lt;/p&gt;

&lt;p&gt;Agency freelancers running multiple small client sites sometimes do a single self-hosted instance with Directus's multi-project capabilities. The economics are good; the maintenance burden is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Directus pricing compares to Sanity
&lt;/h2&gt;

&lt;p&gt;If you are also evaluating Sanity, the comparison is not straightforward because Sanity is API-first with no self-hosting option at all. Sanity's free tier is more generous for small teams (three users, unlimited projects in the free plan), and the Growth plan at $15/seat/month scales differently from Directus Cloud's flat project pricing. I covered the full side-by-side — content model, API design, editor experience, and cost at scale — in the &lt;a href="https://dev.to/sanity-vs-directus-for-next-js-in-2026-an-honest-comparison"&gt;Sanity vs Directus for Next.js comparison post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The short version: Directus is cheaper to run at scale if you self-host and have the ops capability. Sanity is cheaper to get started with for small teams and eliminates infrastructure responsibility entirely.&lt;/p&gt;

</description>
      <category>headlesscms</category>
      <category>comparison</category>
    </item>
    <item>
      <title>How I structure React Next.js patterns across every client project</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 13 Aug 2026 05:54:23 +0000</pubDate>
      <link>https://dev.to/nayankyada/how-i-structure-react-nextjs-patterns-across-every-client-project-1b5k</link>
      <guid>https://dev.to/nayankyada/how-i-structure-react-nextjs-patterns-across-every-client-project-1b5k</guid>
      <description>&lt;p&gt;Every new Next.js project I start has the same skeleton. Not because I copy-paste a boilerplate, but because I've converged on a set of React Next.js patterns that solve the same class of problems every time — server-first rendering, predictable data flow, typed boundaries, and consistent error handling. This post documents those patterns so I can point clients and collaborators at something concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server components by default, client components by exception
&lt;/h2&gt;

&lt;p&gt;The App Router made server components the default. I treat that as a firm rule, not a suggestion. Every file starts as a server component. It only gets the &lt;code&gt;'use client'&lt;/code&gt; directive if it needs browser APIs, event handlers, or client-side state.&lt;/p&gt;

&lt;p&gt;The practical test I run before adding &lt;code&gt;'use client'&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it need &lt;code&gt;useState&lt;/code&gt; or &lt;code&gt;useReducer&lt;/code&gt;? → client.&lt;/li&gt;
&lt;li&gt;Does it need &lt;code&gt;useEffect&lt;/code&gt; or a browser API (&lt;code&gt;window&lt;/code&gt;, &lt;code&gt;IntersectionObserver&lt;/code&gt;)? → client.&lt;/li&gt;
&lt;li&gt;Does it need an event handler (&lt;code&gt;onClick&lt;/code&gt;, &lt;code&gt;onChange&lt;/code&gt;)? → client.&lt;/li&gt;
&lt;li&gt;None of the above? → keep it on the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This matters for bundle size. A server component ships zero JavaScript to the browser. Keeping data-heavy list pages, markdown renderers, and layout wrappers as server components keeps the client bundle lean.&lt;/p&gt;

&lt;p&gt;When I do need interactivity, I push the &lt;code&gt;'use client'&lt;/code&gt; boundary as far down the tree as possible — often to a small leaf component like a &lt;code&gt;&amp;lt;LikeButton&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;MobileMenuToggle&amp;gt;&lt;/code&gt;, not the whole page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colocated data fetching, not a central data layer
&lt;/h2&gt;

&lt;p&gt;I fetch data inside the server component that renders it. No global store, no prop-drilling query results from a layout down to a leaf. Each component owns its data.&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;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;postBySlugQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity/lib/queries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PostBySlugQueryResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;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;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="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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostBySlugQueryResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;postBySlugQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`post:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="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="nc"&gt;PostBody&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js deduplicates &lt;code&gt;fetch&lt;/code&gt; calls with the same URL and options within a single render pass, so two sibling components fetching the same resource don't double-hit the API. For Sanity specifically I use tagged revalidation so deploys stay fast.&lt;/p&gt;

&lt;p&gt;If a page needs data from multiple sources — say a post plus a related-posts sidebar — I fetch in parallel with &lt;code&gt;Promise.all&lt;/code&gt; at the page level and pass results down as props. I don't waterfall fetches through nested components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed API boundaries with Sanity TypeGen
&lt;/h2&gt;

&lt;p&gt;Loose &lt;code&gt;any&lt;/code&gt; types at the data boundary kill refactor confidence. I use Sanity TypeGen to generate TypeScript types from GROQ queries, then import those types at the fetch site.&lt;/p&gt;

&lt;p&gt;The pattern I follow for every query module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sanity/lib/queries.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;defineQuery&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;groq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postBySlugQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineQuery&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] {
    _id,
    title,
    publishedAt,
    "slug": slug.current,
    body,
    mainImage { asset-&amp;gt;, hotspot, crop }
  }
`&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;postListQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  *[_type == "post"] | order(publishedAt desc) [0...$limit] {
    _id,
    title,
    publishedAt,
    "slug": slug.current,
    "excerpt": pt::text(body)[0..120]
  }
`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running &lt;code&gt;sanity typegen generate&lt;/code&gt;, I get &lt;code&gt;PostBySlugQueryResult&lt;/code&gt; and &lt;code&gt;PostListQueryResult&lt;/code&gt; in &lt;code&gt;sanity/types.ts&lt;/code&gt;. Every component that touches post data uses those types. If a schema field gets renamed, TypeScript catches every broken usage before CI runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route organisation that scales
&lt;/h2&gt;

&lt;p&gt;I use Next.js route groups to separate concerns without adding URL segments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  (marketing)/
    page.tsx          # homepage
    about/page.tsx
    layout.tsx        # marketing header/footer
  (content)/
    blog/
      page.tsx        # list
      [slug]/page.tsx # detail
    layout.tsx        # content-specific layout (breadcrumbs, etc.)
  (app)/
    dashboard/
      page.tsx
    layout.tsx        # auth-gated layout
  layout.tsx          # root layout (fonts, providers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each group gets its own layout, so the marketing shell never leaks into the dashboard shell. Route groups also let me colocate related server actions and route handlers near the pages that use them rather than dumping everything into a top-level &lt;code&gt;api/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;For route handlers I keep them thin — parse input, call a service function, return a typed response. The logic lives in &lt;code&gt;lib/&lt;/code&gt;, not in the handler itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error and loading conventions
&lt;/h2&gt;

&lt;p&gt;Every segment that does async work gets three files: &lt;code&gt;page.tsx&lt;/code&gt;, &lt;code&gt;loading.tsx&lt;/code&gt;, and &lt;code&gt;error.tsx&lt;/code&gt;. Not optional — I add them at project creation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;loading.tsx&lt;/code&gt; renders a skeleton that matches the page's visual weight. I size skeleton blocks to match real content dimensions, which prevents CLS when the real content loads. For image-heavy pages the skeleton includes a placeholder div at the same aspect ratio as the image.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;error.tsx&lt;/code&gt; must be a client component (it receives the &lt;code&gt;error&lt;/code&gt; prop and a &lt;code&gt;reset&lt;/code&gt; function). I keep it simple:&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]/error.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&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="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="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"py-24 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-neutral-500"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Something went wrong loading this post.
      &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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 text-sm underline"&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Try again
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&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;pre&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 text-xs text-red-500"&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;pre&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;I never leak error messages in production. The &lt;code&gt;digest&lt;/code&gt; value is logged server-side by Next.js automatically, so I can correlate client errors with server logs without exposing stack traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed server actions for mutations
&lt;/h2&gt;

&lt;p&gt;For forms and mutations I use server actions rather than route handlers so I stay in the RSC model. I wrap every action in a result type so the client component always has a typed response to work with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/actions/subscribe.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&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;z&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;zod&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&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;type&lt;/span&gt; &lt;span class="nx"&gt;ActionResult&lt;/span&gt; &lt;span class="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&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="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;subscribeAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;_prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ActionResult&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid email address.&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="c1"&gt;// call SendGrid / your email service here&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client component calls this with &lt;code&gt;useActionState&lt;/code&gt; (React 19) and renders feedback based on the discriminated union. No &lt;code&gt;try/catch&lt;/code&gt; in the component, no raw error strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this skeleton buys on a real project
&lt;/h2&gt;

&lt;p&gt;These patterns aren't clever. They're boring and consistent, which is exactly the point. When a new developer joins a project or I return to a codebase after three months, the file layout is predictable, the data flow is traceable from query definition to render, and TypeScript catches regressions at the type level before they reach QA. That consistency is worth more than any individual optimisation.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>Hire a developer for your startup: scope by outcome, not hours</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 12 Aug 2026 05:51:56 +0000</pubDate>
      <link>https://dev.to/nayankyada/hire-a-developer-for-your-startup-scope-by-outcome-not-hours-3k15</link>
      <guid>https://dev.to/nayankyada/hire-a-developer-for-your-startup-scope-by-outcome-not-hours-3k15</guid>
      <description>&lt;p&gt;When founders come to me asking how to hire a developer for a startup website or MVP, the first thing I tell them is this: the scope conversation matters more than the CV review. Most early-stage projects fail to ship not because the developer was bad, but because nobody agreed on what 'done' meant before the first invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define done before you talk to anyone
&lt;/h2&gt;

&lt;p&gt;Hourly rate is a distraction at the MVP stage. What you need is an outcome definition — a short list of things that are true when the project is complete. Not features, outcomes.&lt;/p&gt;

&lt;p&gt;Bad scope: "Build a landing page with a CMS."&lt;/p&gt;

&lt;p&gt;Good scope: "Editors can publish a blog post without developer involvement. The contact form sends to our shared inbox. Lighthouse performance score is above 85. The site is live on our domain with SSL."&lt;/p&gt;

&lt;p&gt;When you scope by outcome, you can get a fixed-price quote. When you scope by hours, you get an invoice that grows until someone runs out of patience or money.&lt;/p&gt;

&lt;p&gt;Write your outcomes down in a short brief before you contact a single developer. Three to five bullet points is enough. If you can't write them down, you're not ready to hire — you need one more internal conversation first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each budget tier actually buys in 2026
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Budget (USD)&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;What you don't get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under $3,000&lt;/td&gt;
&lt;td&gt;Template customisation, basic Webflow or WordPress build, minimal CMS setup&lt;/td&gt;
&lt;td&gt;Custom architecture, performance tuning, API integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$3,000–$8,000&lt;/td&gt;
&lt;td&gt;Custom Next.js marketing site with Sanity CMS, basic SEO, contact form, analytics wiring&lt;/td&gt;
&lt;td&gt;Complex e-commerce, authentication, multi-locale, ongoing support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$8,000–$18,000&lt;/td&gt;
&lt;td&gt;Full marketing site or lightweight product with headless CMS, solid Core Web Vitals, structured data, staging environment, handoff documentation&lt;/td&gt;
&lt;td&gt;Enterprise integrations, design from scratch (budget assumes design exists or is bought separately)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$18,000–$40,000&lt;/td&gt;
&lt;td&gt;Complete MVP with auth, payments, content management, performance monitoring, CI/CD pipeline&lt;/td&gt;
&lt;td&gt;Scale-ready infrastructure (that comes later, once you have users)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$40,000+&lt;/td&gt;
&lt;td&gt;Dedicated team time, iterative sprints, ongoing feature development&lt;/td&gt;
&lt;td&gt;A guarantee the product will succeed — no one can sell you that&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are project rates, not hourly projections. A developer quoting you $120/hr with an open-ended timeline can easily land in a higher bucket than a flat-fee quote for the same outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freelancer vs agency at MVP stage
&lt;/h2&gt;

&lt;p&gt;This question comes up in nearly every early conversation. The honest answer is that for most startups at MVP stage, a single experienced freelancer outperforms a small agency — not always, but often enough to be the default recommendation.&lt;/p&gt;

&lt;p&gt;Here's why. A four-person agency charging $15,000 for your MVP will typically assign a junior developer to execute while a senior manages the account. A solo developer charging the same amount is the person who writes every line. At MVP scale, the surface area is small enough that one good person can hold all of it in their head. That matters for speed and consistency.&lt;/p&gt;

&lt;p&gt;Where agencies make sense: when you genuinely need parallel workstreams (design, development, and copywriting happening simultaneously), when you need a named point of contact for board or investor reporting, or when your timeline is under six weeks and a single developer would be a bottleneck.&lt;/p&gt;

&lt;p&gt;What to ask a freelancer: Can you show me a project you scoped, built, and handed off without a team? What does your handoff documentation look like? Who do I call if something breaks after launch?&lt;/p&gt;

&lt;p&gt;What to ask an agency: Who specifically will write the code? What is their seniority level? Can I speak with them directly?&lt;/p&gt;

&lt;h2&gt;
  
  
  Common founder mistakes that burn runway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hiring before the design exists.&lt;/strong&gt; Developers cannot build efficiently against a Figma file that is still changing. Lock the design to at least mid-fidelity wireframes before development starts. Every revision mid-build costs two to four times what it would have cost in design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimising for lowest hourly rate.&lt;/strong&gt; A $40/hr developer who takes four months costs more than a $120/hr developer who takes six weeks. Do the arithmetic before you anchor on rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping the staging environment.&lt;/strong&gt; Launching straight to production without a staging URL where you can review and break things is a false economy. Any competent developer will insist on one. If they don't, that's a flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not defining who owns the infrastructure.&lt;/strong&gt; Before work starts, get written agreement on who holds the hosting account, the domain registrar login, the CMS project, and the repository. Founders who skip this step sometimes find they need the developer's cooperation just to change their own DNS settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating post-launch support as implied.&lt;/strong&gt; Define it explicitly. 'I'll be around' is not a support agreement. A two-week post-launch support window at a fixed rate is reasonable to ask for in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate candidates quickly
&lt;/h2&gt;

&lt;p&gt;You don't need a technical co-founder to vet a developer. You need three things:&lt;/p&gt;

&lt;p&gt;A live URL of something they built. Not a mockup, a deployed site. Visit it on mobile. Check how fast it loads. Look at the page source. If the basics are sloppy in their portfolio work, they will be sloppy in yours.&lt;/p&gt;

&lt;p&gt;A scope conversation, not a pitch. A good developer will ask you clarifying questions before quoting. If someone sends a price within twenty minutes of your first message without asking anything, they either have a template they're applying to every project or they're not thinking carefully about yours.&lt;/p&gt;

&lt;p&gt;A clear handoff expectation. Ask: 'What do I receive at the end of the project?' You want to hear: a repository you own, credentials documented, a brief README or handoff doc, and a walkthrough call. If the answer is vague, the handoff will be vague.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing worth spending money on before hiring
&lt;/h2&gt;

&lt;p&gt;If your budget is tight and you're uncertain whether you need a full build at all, spend $300–$500 on a two-hour scoping session with an experienced developer before committing to anything. Ask them to review your requirements and tell you the fastest path to a working MVP. The session pays for itself if it saves you one bad hire or one month of rework.&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>nextjs</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Freelance web developer for hire: what a good one actually delivers</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 11 Aug 2026 05:33:21 +0000</pubDate>
      <link>https://dev.to/nayankyada/freelance-web-developer-for-hire-what-a-good-one-actually-delivers-5ejd</link>
      <guid>https://dev.to/nayankyada/freelance-web-developer-for-hire-what-a-good-one-actually-delivers-5ejd</guid>
      <description>&lt;p&gt;When you're looking for a freelance web developer for hire, the gap between a smooth engagement and a frustrating one usually comes down to process, not skill. Most developers who struggle with clients aren't bad at code — they're bad at setting expectations. This post lays out what a professional engagement looks like, so you know what to ask for and what to walk away from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication cadence: how often you should actually hear from them
&lt;/h2&gt;

&lt;p&gt;A solo freelancer won't — and shouldn't — be updating you every hour. But silence for more than two or three business days during active development is a red flag. Here's what a reasonable cadence looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project kick-off:&lt;/strong&gt; A written summary of what's being built, in what order, and what decisions still need your input. This goes out within 24 hours of the project starting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly async update:&lt;/strong&gt; A short message (Slack, email, whatever you agreed on) covering what was completed, what's in progress, and any blockers waiting on you. Not a meeting — a message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ad-hoc flags:&lt;/strong&gt; If something discovered mid-build changes the scope or timeline, you hear about it immediately, not at the weekly update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time:&lt;/strong&gt; Messages during business hours get a reply within a few hours, not days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a developer tells you they'll "be in touch when there's something to show," that's not a communication plan — it's a way to disappear for two weeks and call it focused work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staging URLs: you should always have access
&lt;/h2&gt;

&lt;p&gt;Every project, regardless of size, should have a staging URL you can visit. Not screenshots. Not a screen-share call where the developer controls what you see. A live URL you can open in your own browser, on your own device, and share with your team.&lt;/p&gt;

&lt;p&gt;For a Next.js project on Vercel, this costs nothing — every push to a branch generates a preview URL automatically. There's no excuse for skipping this.&lt;/p&gt;

&lt;p&gt;What a staging environment gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A place to review work before it goes live&lt;/li&gt;
&lt;li&gt;A real URL to send to stakeholders or investors without touching production&lt;/li&gt;
&lt;li&gt;Confidence that the developer is building something real, not just promising it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a developer asks you to wait until the project is "done" before they share anything, that's a process problem. Iterative review is how you avoid paying for something that misses the brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed-price vs hourly: which to choose and when
&lt;/h2&gt;

&lt;p&gt;Both models are legitimate. The right choice depends on how well-defined the project is.&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;Better model&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Well-defined scope, agreed deliverables&lt;/td&gt;
&lt;td&gt;Fixed-price&lt;/td&gt;
&lt;td&gt;You know the cost upfront; developer has an incentive to finish efficiently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evolving requirements, ongoing work&lt;/td&gt;
&lt;td&gt;Hourly / retainer&lt;/td&gt;
&lt;td&gt;Fair to both sides when the scope isn't locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP with unknowns&lt;/td&gt;
&lt;td&gt;Fixed-price for Phase 1, hourly after&lt;/td&gt;
&lt;td&gt;Caps discovery cost while preserving flexibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrations with third-party APIs&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;API behavior is often unpredictable; fixed-price punishes the developer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fixed-price doesn't mean no surprises — it means surprises are scoped separately. A good developer will write a change order for anything that wasn't in the original brief rather than silently absorbing it (and resenting you for it) or silently billing extra (and surprising you).&lt;/p&gt;

&lt;p&gt;Red flag: a fixed-price quote with no written specification attached. Without a spec, "fixed price" is meaningless — there's no shared definition of done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handover: what you should receive when the project closes
&lt;/h2&gt;

&lt;p&gt;A project isn't finished when it goes live. It's finished when you can operate it without the developer. That means handover is a deliverable, not an afterthought.&lt;/p&gt;

&lt;p&gt;At minimum, expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository access:&lt;/strong&gt; The code lives in your GitHub/GitLab account, not theirs. You should have been invited as owner from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables documented:&lt;/strong&gt; Every API key, secret, and configuration value listed in a private document (not in the repo), with a note on where each one is managed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CMS editor walkthrough:&lt;/strong&gt; If the project uses Sanity Studio or any other CMS, a recorded or written walkthrough of how editors use it — adding pages, publishing posts, updating images — so your team isn't dependent on the developer for routine content changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment process:&lt;/strong&gt; Where the project is hosted, how deploys are triggered, and who has access to the hosting dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party service accounts:&lt;/strong&gt; Domain registrar, email sending, analytics, CDN — you should own every account, not the freelancer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these are missing, the project isn't actually done. Handover gaps are one of the most common reasons clients get stuck after a freelancer engagement ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warranty period: what's reasonable to expect
&lt;/h2&gt;

&lt;p&gt;A warranty doesn't mean the developer is on-call forever. It means there's an agreed window after launch during which bugs found in the original scope are fixed at no additional charge.&lt;/p&gt;

&lt;p&gt;Two weeks is the minimum. Thirty days is standard for a mid-size project. Ninety days is reasonable for a complex build with integrations.&lt;/p&gt;

&lt;p&gt;What a warranty covers: functionality that was promised and doesn't work as specified.&lt;/p&gt;

&lt;p&gt;What it doesn't cover: new feature requests, design changes, or bugs caused by third-party services changing their APIs.&lt;/p&gt;

&lt;p&gt;Get this in writing before the project starts — not after something breaks.&lt;/p&gt;

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

&lt;p&gt;A good freelance web developer runs a professional process regardless of project size. You get a staging URL early, regular written updates, a clear pricing model with a written spec attached, and a handover that leaves you in control. If a developer can't explain how they handle these things before you hire them, that's your answer.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sanitycms</category>
      <category>headlesscms</category>
      <category>seo</category>
    </item>
    <item>
      <title>Tailwind Next.js starter: create-next-app vs popular boilerplates</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:53:11 +0000</pubDate>
      <link>https://dev.to/nayankyada/tailwind-nextjs-starter-create-next-app-vs-popular-boilerplates-d4g</link>
      <guid>https://dev.to/nayankyada/tailwind-nextjs-starter-create-next-app-vs-popular-boilerplates-d4g</guid>
      <description>&lt;p&gt;Picking a tailwind Next.js starter sounds trivial until you're six months in and your chosen boilerplate hasn't touched its dependency tree since the Pages Router was current. This post compares the two main paths — &lt;code&gt;create-next-app&lt;/code&gt; with manual Tailwind wiring versus grabbing a community starter — and gives you a concrete checklist for telling the difference between a starter that will serve you and one that will quietly rot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "aging well" actually means for a starter
&lt;/h2&gt;

&lt;p&gt;A starter ages well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It tracks the Next.js App Router and drops Pages Router patterns as those patterns become legacy.&lt;/li&gt;
&lt;li&gt;Its Tailwind config doesn't lock you to v3 conventions that don't translate to v4 (the &lt;code&gt;@import "tailwindcss"&lt;/code&gt; directive, CSS-first config, the new &lt;code&gt;@theme&lt;/code&gt; block).&lt;/li&gt;
&lt;li&gt;Its dependency list is short and each dependency has an active maintainer.&lt;/li&gt;
&lt;li&gt;It uses &lt;code&gt;next/image&lt;/code&gt; with explicit &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, or &lt;code&gt;fill&lt;/code&gt; — not wrapped in a custom component that papers over CLS.&lt;/li&gt;
&lt;li&gt;TypeScript is strict from day one, not added as an afterthought with &lt;code&gt;@ts-ignore&lt;/code&gt; patches everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those criteria filter out a lot of what you'll find on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: create-next-app + manual Tailwind
&lt;/h2&gt;

&lt;p&gt;This is what I use on client projects. The command:&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;# Creates App Router project, TypeScript, ESLint, src/ directory, no src/app/page default styles&lt;/span&gt;
npx create-next-app@latest my-site &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--typescript&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--eslint&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--src-dir&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-import-alias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install Tailwind v4 the way the v4 docs say to, not the v3 PostCSS path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss @tailwindcss/vite
&lt;span class="c"&gt;# Or with the PostCSS plugin if you need it:&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss @tailwindcss/postcss postcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a single line to your &lt;code&gt;src/app/globals.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/app/globals.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-geist-sans&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;55%&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt; &lt;span class="m"&gt;250&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;No &lt;code&gt;tailwind.config.ts&lt;/code&gt;. No PostCSS config file unless your project actually needs PostCSS plugins. That's it.&lt;/p&gt;

&lt;p&gt;This path wins on longevity because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;create-next-app&lt;/code&gt; is the official scaffold — it will track App Router, Turbopack, and RSC conventions as they move.&lt;/li&gt;
&lt;li&gt;You control every dependency. Nothing sneaks in that you don't know about.&lt;/li&gt;
&lt;li&gt;When Tailwind v5 arrives you read the migration guide and update one package, not untangle a boilerplate author's opinions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cost is setup time. Budget 30–45 minutes to wire up fonts, a base layout, and &lt;code&gt;next-sitemap&lt;/code&gt; or the App Router &lt;code&gt;sitemap.ts&lt;/code&gt; convention. That's a one-time cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: community starters and boilerplates
&lt;/h2&gt;

&lt;p&gt;The most-starred Tailwind Next.js starters on GitHub (as of mid-2026) include next-starter, taxonomy (shadcn's older reference app), and a long tail of personal-portfolio templates. Here's how they compare on the criteria that matter.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;create-next-app + manual&lt;/th&gt;
&lt;th&gt;Actively maintained starter&lt;/th&gt;
&lt;th&gt;Abandoned starter (&amp;gt;12 months stale)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App Router native&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Usually ✓&lt;/td&gt;
&lt;td&gt;Often Pages Router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind v4 config&lt;/td&gt;
&lt;td&gt;✓ (you set it)&lt;/td&gt;
&lt;td&gt;Varies — check the config file&lt;/td&gt;
&lt;td&gt;Typically v3, sometimes v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript strict mode&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency count&lt;/td&gt;
&lt;td&gt;~6 prod deps&lt;/td&gt;
&lt;td&gt;15–40+&lt;/td&gt;
&lt;td&gt;15–40+, often outdated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;next/image correct usage&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Sometimes — check for CLS hacks&lt;/td&gt;
&lt;td&gt;Often broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last commit&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Check GitHub&lt;/td&gt;
&lt;td&gt;Often 2023 or earlier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ejectable&lt;/td&gt;
&lt;td&gt;Trivially&lt;/td&gt;
&lt;td&gt;Hard — opinions baked in&lt;/td&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The taxonomy app is worth studying as a reference, but I would not scaffold new client work from it — it was built to demonstrate shadcn component patterns, not to be a generic starting point, and its dependency surface is wide.&lt;/p&gt;

&lt;p&gt;If you want a community starter, the ones worth considering in 2026 are the ones where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repo has commits in the last 60 days.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; shows &lt;code&gt;next&lt;/code&gt; at 15.x, &lt;code&gt;tailwindcss&lt;/code&gt; at 4.x.&lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;tailwind.config.ts&lt;/code&gt; or &lt;code&gt;tailwind.config.js&lt;/code&gt; at the root — the v4 config lives in CSS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"strict": true&lt;/code&gt; is set in &lt;code&gt;tsconfig.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The README acknowledges App Router, not just Pages Router patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those fail, move on. Boilerplate debt compounds faster than application debt because it's in your foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shadcn/ui question
&lt;/h2&gt;

&lt;p&gt;Shadcn/ui is a component distribution, not a starter — it adds components into your source tree rather than installing a package. That distinction matters because you own the code. If you want Radix primitives styled with Tailwind, the right move is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scaffold with &lt;code&gt;create-next-app&lt;/code&gt; as above.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npx shadcn@latest init&lt;/code&gt; to add the component scaffolding.&lt;/li&gt;
&lt;li&gt;Add only the components you actually need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not grab a "shadcn starter" repo that pre-installs all 40+ components. You will carry that weight even if you use three of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags in any starter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;next.config.js&lt;/code&gt; that disables &lt;code&gt;reactStrictMode&lt;/code&gt; — usually done to hide double-render bugs rather than fix them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;images.domains&lt;/code&gt; in next config instead of &lt;code&gt;images.remotePatterns&lt;/code&gt; — deprecated in Next.js 14, removed behaviour in 15.&lt;/li&gt;
&lt;li&gt;A custom &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; wrapper that passes &lt;code&gt;layout="fill"&lt;/code&gt; (v12-era API).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@apply&lt;/code&gt; used everywhere in CSS — fine in v3, still works in v4, but often signals the project was written before v4's utility-in-CSS approach was available.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;postinstall&lt;/code&gt; script that patches &lt;code&gt;node_modules&lt;/code&gt; directly.&lt;/li&gt;
&lt;li&gt;Any dependency pinned with &lt;code&gt;"next": "13.x"&lt;/code&gt; or similar hard ceiling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical recommendation
&lt;/h2&gt;

&lt;p&gt;For a production Next.js project that needs to last 18–24 months without a painful migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;create-next-app&lt;/code&gt; with the flags above.&lt;/li&gt;
&lt;li&gt;Wire Tailwind v4 manually — it's four lines.&lt;/li&gt;
&lt;li&gt;If you need UI components, add shadcn selectively via its CLI, not a pre-bundled starter.&lt;/li&gt;
&lt;li&gt;Commit a &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;engines&lt;/code&gt; field locking to Node 22.x so your CI and your client's server stay in sync.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Community starters have their place — especially for portfolio sites where setup speed matters and longevity less so. But for anything where a client will be maintaining or extending the codebase in 12 months, the manual path is the only one I'd hand off with confidence.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>API CMS explained: what it's good at and where it breaks down</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sun, 09 Aug 2026 05:29:08 +0000</pubDate>
      <link>https://dev.to/nayankyada/api-cms-explained-what-its-good-at-and-where-it-breaks-down-1ip2</link>
      <guid>https://dev.to/nayankyada/api-cms-explained-what-its-good-at-and-where-it-breaks-down-1ip2</guid>
      <description>&lt;p&gt;An API CMS — sometimes called an API-first or headless CMS — decouples content storage from content presentation. Instead of generating HTML server-side like WordPress does, it exposes your content through an HTTP or GraphQL API, and every consuming application — your Next.js site, your mobile app, your digital signage, your email renderer — fetches exactly what it needs. That flexibility is the whole pitch. Whether it pays off depends heavily on what your team actually builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an API CMS is genuinely good at
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Omnichannel content delivery
&lt;/h3&gt;

&lt;p&gt;The strongest case for an API CMS is when the same content genuinely needs to appear in more than one place. A product description that lives in Sanity can feed your Next.js marketing site, your React Native app, your Shopify storefront, and a third-party partner API — all from one source of truth, all pulling from the same GROQ or REST endpoint.&lt;/p&gt;

&lt;p&gt;With a traditional CMS, you'd be copying content between systems or building fragile sync jobs. With an API-first system, the content model is the integration point. Schema changes propagate to every consumer automatically, because they're all reading from the same API.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. I've worked on projects where the same &lt;code&gt;product&lt;/code&gt; document type in Sanity serves a Next.js e-commerce front end and a weekly email digest rendered by a Node.js script hitting the same Content Lake. The editorial team edits once. The field &lt;code&gt;description&lt;/code&gt; shows up correctly in both places without any manual export.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer control over the data layer
&lt;/h3&gt;

&lt;p&gt;A traditional CMS like WordPress gives you pre-shaped data — a post has a title, body, excerpt, and featured image. If you want something different, you're writing PHP plugins or fighting the REST API.&lt;/p&gt;

&lt;p&gt;An API CMS gives you a blank schema. You define exactly what a &lt;code&gt;caseStudy&lt;/code&gt; or &lt;code&gt;landingPage&lt;/code&gt; document contains, what types are allowed in a Portable Text field, what references are valid. In Sanity, that schema lives as TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sanity/schemas/caseStudy.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineField&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;caseStudy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caseStudy&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;Case study&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;industry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outcomes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relatedPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="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;You own the data model. The CMS stores and serves it. That's a meaningful shift in control compared to fighting a plugin ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance through selective fetching
&lt;/h3&gt;

&lt;p&gt;Because you write your own queries, you fetch only what the page needs. A GROQ query for a blog listing page grabs slugs, titles, and cover image metadata — nothing else. The payload is small, the response is fast, and Next.js can cache it at the edge.&lt;/p&gt;

&lt;p&gt;With a traditional CMS, you often get the whole document whether you want it or not, and either strip it client-side (wasteful) or write a custom endpoint (which is just reinventing API-first architecture anyway).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where an API CMS falls short
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Editor experience is an afterthought
&lt;/h3&gt;

&lt;p&gt;This is the honest part most developer blog posts skip. API-first CMSs are designed by developers for developers. The editing interface — Sanity Studio, Contentful's web app, whatever Strapi ships — is functional, but it's not polished for non-technical editors.&lt;/p&gt;

&lt;p&gt;WordPress has fifteen years of UX iteration aimed at writers and marketing teams. Sanity Studio is excellent by headless standards, but an editor coming from WordPress or Squarespace will still face a learning curve. If your client's content team is five non-technical marketers who publish daily, that friction is real and it lands on your project budget as training time and support tickets.&lt;/p&gt;

&lt;p&gt;The risk isn't that the CMS can't do it — it's that no one budgets for the onboarding properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preview is genuinely complex
&lt;/h3&gt;

&lt;p&gt;In a traditional CMS, preview is built in. Click "Preview" and you see the page with draft content. In a headless setup, preview requires explicit engineering: a draft mode endpoint in Next.js, a secret token, a Sanity Presentation tool configuration, and a way to pass that context through your entire rendering tree.&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/api/draft/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;draftMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirect&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SANITY_PREVIEW_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;draftMode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the minimal version. Add Sanity's Presentation overlay, overlay components for visual editing, and CORS configuration for the studio domain, and you've got a non-trivial amount of infrastructure just so an editor can see their changes before publishing. It works — I use it on production projects — but it's engineering effort that traditional CMS users get for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glue code accumulates fast
&lt;/h3&gt;

&lt;p&gt;An API CMS solves content storage and delivery. Everything else — sitemap generation, redirect management, form handling, search indexing, email notifications on publish, image transformation pipelines — is your responsibility. You wire it up.&lt;/p&gt;

&lt;p&gt;Sanity doesn't email your team when a post is published. You build a webhook handler and call SendGrid. Sanity doesn't update your Algolia index. You write the sync logic. Sanity doesn't generate a sitemap. You write the Next.js route handler.&lt;/p&gt;

&lt;p&gt;None of this is hard, but every piece is custom code you now own and maintain. Projects that start with "we just need a blog" end up with 800 lines of infrastructure across webhooks, revalidation handlers, preview routes, and sitemap generators. That's fine if you've budgeted for it. It's a nasty surprise if you assumed the CMS handled it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost scales with API calls, not seat count
&lt;/h3&gt;

&lt;p&gt;Most API-first CMSs price on API requests, bandwidth, or document count — not on how many editors use the system. That's great for small teams. At scale, the math shifts. A site doing millions of page views with aggressive revalidation can hit CDN bandwidth limits or API rate limits on plans that seemed generous at the start.&lt;/p&gt;

&lt;p&gt;Sanity's Growth plan includes 500k API CDN requests per month. At 10 requests per page build and aggressive ISR revalidation, that ceiling is reachable on a moderately trafficked site without careful query batching and edge caching in front of the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an API CMS is the right call
&lt;/h2&gt;

&lt;p&gt;Choose an API-first CMS when: you're building for more than one channel; your content model is genuinely custom; you have at least one developer who owns the integration long-term; and your editors are comfortable learning a new tool with proper onboarding.&lt;/p&gt;

&lt;p&gt;Avoid it when: the site is a simple brochure with a non-technical owner who will maintain it themselves; your budget doesn't include the glue code engineering hours; or preview and draft workflow are dealbreakers that need to work on day one without custom work.&lt;/p&gt;

&lt;p&gt;The API CMS model gives developers real leverage over the content layer. The cost is that you're building the rest of the system yourself — and that cost is real, recurring, and worth naming before you start.&lt;/p&gt;

</description>
      <category>headlesscms</category>
      <category>sanitycms</category>
      <category>nextjs</category>
      <category>groq</category>
    </item>
    <item>
      <title>Next.js as a backend: honest limits for production apps</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Sat, 08 Aug 2026 05:14:07 +0000</pubDate>
      <link>https://dev.to/nayankyada/nextjs-as-a-backend-honest-limits-for-production-apps-3g3k</link>
      <guid>https://dev.to/nayankyada/nextjs-as-a-backend-honest-limits-for-production-apps-3g3k</guid>
      <description>&lt;p&gt;The Next.js backend question comes up on almost every project scoping call I take. A founder wants to ship fast, they already have Next.js on the frontend, and the natural question is: can we just use route handlers for the API too? The honest answer is yes, often — but with specific limits that will bite you if you ignore them.&lt;/p&gt;

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

&lt;p&gt;Route handlers (&lt;code&gt;app/api/*/route.ts&lt;/code&gt;) are proper HTTP endpoints. They run on Node.js or the Edge runtime, support all HTTP methods, can read cookies and headers, and stream responses. Server Actions give you a direct RPC-style bridge from client components to server code without writing an endpoint at all. For a large category of product work — form submissions, authenticated CRUD, sending emails via SendGrid, triggering Sanity mutations, webhook ingestion — this is genuinely sufficient.&lt;/p&gt;

&lt;p&gt;I have production apps where the entire "backend" is a dozen route handlers and a handful of server actions. No Express server, no separate deployment, no ops overhead. It works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/contact/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/sendgrid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// basic validation omitted for brevity&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&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;CONTACT_EMAIL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;body&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real contact endpoint. Deployed to Vercel, zero extra infrastructure, cold starts are under 300ms in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it genuinely holds up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Authenticated REST-style APIs for your own frontend.&lt;/strong&gt; If the only consumer of your API is your Next.js frontend, route handlers are a perfect fit. You share types directly, you can use server actions instead of fetch calls, and you don't need to design a public API contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lightweight webhook receivers.&lt;/strong&gt; Stripe webhooks, Sanity webhooks, GitHub Actions callbacks — these are short-lived, low-concurrency endpoints. Route handlers handle them fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Actions for mutations.&lt;/strong&gt; For forms, optimistic UI, and simple write paths, server actions remove a layer of indirection. The mental model is simpler and TypeScript inference works end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge middleware for auth guards.&lt;/strong&gt; Running token validation at the edge in &lt;code&gt;middleware.ts&lt;/code&gt; before any page or API handler runs is genuinely useful and hard to replicate cheaply outside of Next.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real limits — where it starts to break
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Background jobs and queues.&lt;/strong&gt; Vercel functions time out. The default is 10 seconds on Hobby, 60 seconds on Pro, 800 seconds on Enterprise. If you need to process a video, crunch a large dataset, or run a job that should survive a deploy, a route handler is the wrong tool. You need a proper queue — BullMQ on a VPS, Inngest, Trigger.dev — with Next.js acting only as the enqueue point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-lived connections.&lt;/strong&gt; WebSockets are not natively supported in Vercel's serverless model. You can use streaming responses for one-directional pushes, but anything requiring a persistent bidirectional channel needs a separate service — Ably, Pusher, Soketi, or a dedicated Node server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party API consumers.&lt;/strong&gt; The moment a mobile app, a partner integration, or a separate service needs to call your API, you need versioning, stable contracts, proper auth (JWT issuance, OAuth), and documentation. Route handlers can technically serve this, but you're now fighting against Next.js's opinionated file structure rather than working with it. At that point, a standalone Fastify or Hono service is cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy compute.&lt;/strong&gt; CPU-bound tasks — image processing, PDF generation, ML inference — should not live inside a serverless function you're paying per-invocation for, especially when they might time out mid-work. Offload these to a dedicated worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database connection pooling at scale.&lt;/strong&gt; Every invocation of a serverless route handler can open a new database connection. With PostgreSQL this will exhaust your connection limit under real traffic unless you're using PgBouncer or Neon's HTTP driver. This isn't unique to Next.js, but it's a sharper problem here than on a traditional server that holds a single connection pool.&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;// This pattern will exhaust Postgres connections at scale on serverless&lt;/span&gt;
&lt;span class="c1"&gt;// Use @neondatabase/serverless or a connection pooler instead&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;Pool&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;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ❌ new Pool() per cold start = connection leak&lt;/span&gt;

&lt;span class="c1"&gt;// Better:&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;neon&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;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ✅ HTTP-based, no persistent conn&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;neon&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;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pattern I actually use
&lt;/h2&gt;

&lt;p&gt;For most marketing + product sites I build, Next.js handles 90% of the backend surface: content mutations via Sanity, auth via NextAuth, transactional emails, simple CRUD. That covers a founder's MVP and often the first 18 months of a real product.&lt;/p&gt;

&lt;p&gt;When the project needs background processing or a public API, I introduce a second service — usually a lightweight Hono app deployed to a $6/month VPS or a Fly.io instance — and keep Next.js as the user-facing layer. The two services share a TypeScript types package if they're in the same monorepo. This boundary keeps Next.js doing what it's good at and removes the pressure to hack around serverless constraints.&lt;/p&gt;

&lt;p&gt;The anti-pattern I see most often is teams discovering these limits after going to production and then spending two weeks retrofitting a queue system into a codebase that wasn't designed for it. Identifying the constraints early — specifically: do we need background jobs, do we need WebSockets, do we need a public API — takes 20 minutes in a scoping call and saves weeks later.&lt;/p&gt;

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

&lt;p&gt;Next.js is a capable backend for a well-defined category of work. It is not a general-purpose application server. Treat it like one and you'll hit a wall. Know the wall before you start building, and it'll serve you well for a long time.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Webflow vs Next.js cost in 2026: where the curve crosses</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:55:57 +0000</pubDate>
      <link>https://dev.to/nayankyada/webflow-vs-nextjs-cost-in-2026-where-the-curve-crosses-3onp</link>
      <guid>https://dev.to/nayankyada/webflow-vs-nextjs-cost-in-2026-where-the-curve-crosses-3onp</guid>
      <description>&lt;p&gt;The &lt;strong&gt;Webflow vs Next.js cost&lt;/strong&gt; question comes up every time a founder is weighing a fast launch against long-term flexibility. The honest answer is that Webflow is genuinely cheaper for the first twelve months on a simple marketing site — but the curve crosses sooner than most Webflow advocates admit, and it crosses hard once you add seats, a CMS, or a developer who needs to extend anything.&lt;/p&gt;

&lt;p&gt;This post breaks down real platform costs, developer time, and the hidden line items that don't show up in a pricing page screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually pay for on each side
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Webflow&lt;/strong&gt; bundles hosting, CDN, CMS, and visual editor into one monthly bill. That's the genuine advantage. You're not juggling Vercel + Sanity + a developer's hourly rate to keep the lights on. For a one-person marketing team publishing blog posts, it's a reasonable call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js + CMS&lt;/strong&gt; (I'll use Sanity as the CMS baseline here because it's what I build on and its free tier is the most generous in its class) splits costs into three buckets: hosting, CMS, and developer time. Hosting on Vercel Hobby is free for non-commercial projects; Pro is $20/month per member. Sanity is free up to 2 users and 10k API requests/day. Those numbers stay at zero longer than most people expect for a new site.&lt;/p&gt;

&lt;p&gt;The catch is that someone has to wire these together, and that person costs money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 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;Webflow (annual)&lt;/th&gt;
&lt;th&gt;Next.js + Sanity + Vercel (annual)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo founder, static marketing site, no blog&lt;/td&gt;
&lt;td&gt;$23/mo Basic = &lt;strong&gt;$276&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Hobby $0 + Sanity Free $0 + ~8 hr setup @ $80/hr = &lt;strong&gt;$640&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-page site + blog, 1 editor&lt;/td&gt;
&lt;td&gt;$39/mo CMS = &lt;strong&gt;$468&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Pro $20/mo + Sanity Free $0 + ~20 hr build = &lt;strong&gt;$1,840&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-page site + blog, 3 editors&lt;/td&gt;
&lt;td&gt;$39/mo CMS + 2 extra seats @ $9/mo = &lt;strong&gt;$684&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Pro $20/mo + Sanity Free (3 users covered) + ~20 hr build = &lt;strong&gt;$1,840&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-page site + blog, 5 editors, custom forms&lt;/td&gt;
&lt;td&gt;$74/mo Business = &lt;strong&gt;$888&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Pro $20/mo + Sanity Growth $15/mo + ~40 hr build = &lt;strong&gt;$3,380&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce or app logic, 5 editors&lt;/td&gt;
&lt;td&gt;Webflow Commerce from $74/mo + 2% txn fee = &lt;strong&gt;$888 + fees&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Pro $20/mo + Sanity Growth $15/mo + ~80 hr custom dev = &lt;strong&gt;$6,620&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing editorial + design changes, in-house dev&lt;/td&gt;
&lt;td&gt;$74/mo Business + dev time at Webflow API limits = &lt;strong&gt;$888 + friction&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Vercel Pro $20/mo + Sanity Growth $15/mo + dev owns codebase = &lt;strong&gt;$420 recurring&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Developer rate used: $80/hour (mid-market freelancer in Western markets; adjust for your situation). Build hours are one-time, not annual, but I've included them here because they're the dominant cost in year one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Webflow genuinely wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No developer on staff, simple site.&lt;/strong&gt; If you're a founder who can use the Webflow designer yourself, rows 1 and 2 in that table are clear wins for Webflow. You pay $276–$468/year and you're live. A Next.js project requires someone to build it, and even a lean build from a competent freelancer will run $1,500–$2,500 for something production-ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed to first publish.&lt;/strong&gt; Webflow's editor is polished. A non-technical marketer can publish a landing page without filing a ticket. The equivalent in a custom Next.js stack — Sanity Studio plus a deployment pipeline — takes a few hours to configure and a couple of hours to train someone on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable monthly bill.&lt;/strong&gt; One invoice, one vendor. For a solo operator managing ten other tools, that simplicity has real value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the curve crosses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Seats.&lt;/strong&gt; Webflow charges $9/month per extra editor above the base plan's allowance. Sanity's free tier covers 3 non-admin users; its Growth plan ($15/month) covers up to 10. At 4–5 editors the per-seat cost on Webflow starts compounding noticeably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom logic.&lt;/strong&gt; Webflow's CMS is a fixed schema. The moment you need a content type that doesn't map to its built-in fields — a product comparison table, a localized page, a structured job listing with computed fields — you're either fighting the tool or paying for a workaround. Next.js + Sanity lets a developer define exactly the schema the content team needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codebase ownership.&lt;/strong&gt; Webflow exports HTML/CSS that isn't particularly maintainable. If you ever want a developer to build on top of what you have, they're starting close to scratch anyway. A Next.js repository is a real codebase: version controlled, testable, deployable to any Node host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance ceiling.&lt;/strong&gt; Webflow generates reasonable Core Web Vitals scores for simple pages. Add a complex homepage with animations, multiple third-party scripts, and a rich blog archive, and you lose fine-grained control. With Next.js you control every byte: streaming SSR, ISR cache headers, image formats, bundle splitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-term recurring cost.&lt;/strong&gt; Once a Next.js site is built and live, the recurring bill for a small-to-medium site is $20–$35/month (Vercel Pro + Sanity Growth). Webflow Business runs $74/month. That's a $40–$50/month gap that compounds significantly over a 3–5 year product lifespan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question for founders
&lt;/h2&gt;

&lt;p&gt;It's not "which is cheaper" in the abstract — it's when do you expect to have a developer involved on a recurring basis.&lt;/p&gt;

&lt;p&gt;If the answer is never, Webflow is the right call for a straightforward marketing site. You'll pay more over three years but you'll avoid developer coordination overhead entirely.&lt;/p&gt;

&lt;p&gt;If the answer is occasionally or soon, the math shifts quickly. A developer working in a Next.js + Sanity codebase can move fast and own the full stack. The same developer working inside Webflow's constraints will spend a disproportionate amount of time on workarounds — and you'll still be paying Webflow's platform fee on top.&lt;/p&gt;

&lt;p&gt;The inflection point in practice: once you have more than 3 editors, need any custom content modeling, or expect more than 10–15 developer-hours per year of ongoing work, Next.js + a headless CMS becomes cheaper on a total-cost basis within 18–24 months of launch.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>headlesscms</category>
      <category>comparison</category>
      <category>seo</category>
    </item>
    <item>
      <title>What a web CMS actually owes you: my baseline standard</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:11:37 +0000</pubDate>
      <link>https://dev.to/nayankyada/what-a-web-cms-actually-owes-you-my-baseline-standard-5b46</link>
      <guid>https://dev.to/nayankyada/what-a-web-cms-actually-owes-you-my-baseline-standard-5b46</guid>
      <description>&lt;p&gt;A web CMS is not a database wrapper with a nice UI. It is the operational centre of a content team and the API contract a developer builds against for years. Most platforms fail on at least one of those two jobs. Here is the standard I have arrived at after shipping enough of these projects to know what breaks down in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The draft and preview contract is non-negotiable
&lt;/h2&gt;

&lt;p&gt;Every web CMS I recommend must support a first-class draft state that is completely isolated from published content. Not a "preview" flag on the same document. An actual separate document state that can be routed through a secure preview URL without leaking into public API responses.&lt;/p&gt;

&lt;p&gt;The failure mode I see most often: a CMS that returns drafts in the default query unless you explicitly filter them out. That is backwards. Drafts should require opt-in access — a token, a cookie, an edge flag — not opt-out filtering that a tired developer forgets on a deadline.&lt;/p&gt;

&lt;p&gt;On the Next.js side this pairs with Route Handlers and the draft mode cookie. On the CMS side it requires perspective-aware content delivery: the same GROQ query (or REST call) should return published data by default and draft data only when a valid secret is present. If a platform cannot do that cleanly, editors will break live pages while saving, and that ends trust in the system fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured content, not rich text blobs
&lt;/h2&gt;

&lt;p&gt;A web CMS that stores page content as a single HTML or Markdown string is a publishing tool, not a content system. Structured content means each piece of content is a typed document with discrete fields: a string title, a reference to an author document, an array of typed block content, an image with crop and hotspot metadata stored separately from the URL.&lt;/p&gt;

&lt;p&gt;Why does this matter? Because structured content is queryable, transformable, and renderable in multiple contexts without scraping HTML. You can build a table-of-contents from block headings without parsing HTML. You can serve a plain-text summary to a search index without stripping tags. You can push content to a mobile app, an email template, and a web page from the same source without maintaining three copies.&lt;/p&gt;

&lt;p&gt;Rich text blocks are fine inside a structured document — Portable Text handles this well — but the document itself must be structured. If the CMS treats every page as a freeform WYSIWYG canvas with no schema enforcement, you end up querying a blob and the developer API surface is effectively zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles and permissions that map to real teams
&lt;/h2&gt;

&lt;p&gt;A basic author/admin split is not enough. Real teams have at least three operational roles: someone who writes drafts and can never publish without review, someone who reviews and publishes but cannot touch schema or studio configuration, and a developer-admin who controls everything.&lt;/p&gt;

&lt;p&gt;Beyond role-based access, a mature web CMS needs document-level permissions. A regional marketing team should not be able to edit the global homepage even if they are editors. A contractor should be able to write in one document type and read-only everything else.&lt;/p&gt;

&lt;p&gt;The failure mode here is usually a CMS that has roles but no path rules or document-type filtering. Everyone with the "editor" role can edit every document, and when that causes an incident — and it will — the only fix is a custom middleware layer that the developer has to maintain forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning that editors can actually use
&lt;/h2&gt;

&lt;p&gt;Version history should be a first-class CMS feature, not an afterthought or an enterprise add-on. Editors need to see who changed a document, when, and what changed. They need to restore a previous version without filing a ticket.&lt;/p&gt;

&lt;p&gt;The bar I hold CMS platforms to: every published change creates a named, browsable history entry. Restoring does not require developer access. Diff views show field-level changes, not a JSON dump.&lt;/p&gt;

&lt;p&gt;This is one area where many headless platforms still lag behind WordPress. The irony is that WordPress's revision history is one of the reasons content teams resist migrating even when the technical case for headless is clear. If a modern web CMS cannot match a 2004-era feature, that is a real objection, not a comfort-zone complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer API surface must be typed and predictable
&lt;/h2&gt;

&lt;p&gt;A CMS that requires runtime JSON parsing with no type information is a liability in a TypeScript codebase. The developer API surface I consider baseline in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema definitions in code (not only a GUI), ideally colocated with the codebase so schema and query types travel together&lt;/li&gt;
&lt;li&gt;Generated TypeScript types from the schema so query results are typed at the call site&lt;/li&gt;
&lt;li&gt;A query language that is expressive enough to shape the response — projections, joins, conditionals — so the client receives only what it needs rather than fetching whole documents and discarding 80% of the fields
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// groq/post.ts — generated type from Sanity TypeGen&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;defineQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;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;POST_QUERY_RESULT&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/sanity.types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineQuery&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,
    publishedAt,
    "author": author-&amp;gt;{ name, image },
    body
  }
`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// In your RSC:&lt;/span&gt;
&lt;span class="c1"&gt;// const post: POST_QUERY_RESULT = await client.fetch(POST_QUERY, { slug })&lt;/span&gt;
&lt;span class="c1"&gt;// post.author.name is typed — no casting, no any&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the CMS requires REST with no type generation path, that is workable. If it requires GraphQL with a code-gen step, that is workable. If it requires hand-written interfaces that drift from the actual schema because nothing enforces the contract, that is a maintenance debt that compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a weak web CMS looks like in practice
&lt;/h2&gt;

&lt;p&gt;The tell is usually the handoff. A CMS that fails the standards above produces the same handoff problems every time: editors asking why their draft showed up on the live site, developers maintaining a spreadsheet of "fields that don't exist in the API yet", permission incidents where a contractor deleted a document they should never have touched, and a content type explosion because nobody modelled the schema — they just kept adding pages.&lt;/p&gt;

&lt;p&gt;The right platform enforces discipline through its architecture. Draft isolation, schema-first content, RBAC, versioning, and typed queries are not nice-to-haves. They are the conditions under which a content team can move at speed without breaking production. A web CMS that does not provide all five is asking the developer to build the missing pieces as custom middleware, and that cost never shows up in the vendor's pricing page.&lt;/p&gt;

</description>
      <category>headlesscms</category>
      <category>sanitycms</category>
      <category>draftmode</category>
      <category>groq</category>
    </item>
    <item>
      <title>Hire a Next.js developer who can actually ship production</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:09:23 +0000</pubDate>
      <link>https://dev.to/nayankyada/hire-a-nextjs-developer-who-can-actually-ship-production-n7f</link>
      <guid>https://dev.to/nayankyada/hire-a-nextjs-developer-who-can-actually-ship-production-n7f</guid>
      <description>&lt;p&gt;When you hire a Next.js developer, the risk isn't finding someone who claims the skill — it's hiring someone who has only run &lt;code&gt;create-next-app&lt;/code&gt; and watched a YouTube tutorial. This guide gives you concrete signals to separate developers who can ship and maintain a production Next.js site from those who cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a credible portfolio actually looks like
&lt;/h2&gt;

&lt;p&gt;A live URL is the minimum bar, not proof of quality. Ask for three live sites and verify each one yourself before the first call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run PageSpeed Insights on each URL.&lt;/strong&gt; Open &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;pagespeed.web.dev&lt;/a&gt; and paste the homepage. You want LCP under 2.5 seconds, CLS under 0.1, and INP under 200 ms on mobile. A developer who genuinely owns the performance of a site will know those numbers without looking. If the sites score in the 40s on mobile, that tells you they shipped something but did not own the outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check whether the site actually uses the App Router.&lt;/strong&gt; Open DevTools → Network, hard-refresh, and look at the response headers. An App Router site on Vercel will have &lt;code&gt;x-nextjs-cache&lt;/code&gt; headers and typically &lt;code&gt;content-type: text/html&lt;/code&gt; for RSC payloads on navigation. Alternatively, just ask: "What version of Next.js is this on, and is it Pages Router or App Router?" A developer who built it will answer immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look for evidence of CMS integration, not just static sites.&lt;/strong&gt; A portfolio of five identical blog starters proves nothing. You want at least one site that connects to a real data source — Sanity, Contentful, Payload, a custom API — and shows that they have thought about revalidation strategy (ISR tags, on-demand revalidation, or PPR).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask about the worst production bug they fixed.&lt;/strong&gt; The answer reveals operational maturity. "We had a stale cache after a Sanity publish and had to add tag-based revalidation" is a better signal than "I've never had a bug in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  App Router literacy check
&lt;/h2&gt;

&lt;p&gt;The Pages Router still works, but any developer you hire in 2026 for a new project should be fluent in the App Router. These questions take under five minutes and filter hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"When does a Server Component re-render?"&lt;/strong&gt; The correct answer: it doesn't re-render on the client at all — it fetches fresh HTML/RSC payload on navigation or after a revalidation event. Someone who says "when the state changes" is thinking in Pages Router terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Where would you put a session cookie read — a Server Component or a Client Component?"&lt;/strong&gt; Server Component, using &lt;code&gt;cookies()&lt;/code&gt; from &lt;code&gt;next/headers&lt;/code&gt;. If they reach for &lt;code&gt;useEffect&lt;/code&gt; and a client-side fetch, that's a Pages Router reflex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What is the difference between &lt;code&gt;revalidatePath&lt;/code&gt; and &lt;code&gt;revalidateTag&lt;/code&gt;?"&lt;/strong&gt; &lt;code&gt;revalidatePath&lt;/code&gt; purges a specific URL from the cache. &lt;code&gt;revalidateTag&lt;/code&gt; purges all cached fetches that were tagged with a given string — more granular and more useful for CMS webhook revalidation. A developer who has wired up a real CMS will know this distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How would you handle a component that needs both static generation and user-specific data?"&lt;/strong&gt; The right answer in 2026 is Partial Prerendering: wrap the static shell in a normal async Server Component, put the personalised part behind a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, and let Next.js stream it from the edge. Alternatively, route-level SSR with &lt;code&gt;export const dynamic = 'force-dynamic'&lt;/code&gt; for simpler cases. If they cannot articulate any of this, they have not shipped PPR or streaming SSR in a real project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance red flags to watch for
&lt;/h2&gt;

&lt;p&gt;These show up in code review or in a short technical conversation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fetching data inside Client Components with &lt;code&gt;useEffect&lt;/code&gt; when a Server Component would do.&lt;/strong&gt; This pushes network waterfalls onto the browser and increases LCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;sizes&lt;/code&gt; prop on &lt;code&gt;next/image&lt;/code&gt;.&lt;/strong&gt; Every &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; without a &lt;code&gt;sizes&lt;/code&gt; attribute defaults to &lt;code&gt;100vw&lt;/code&gt;, forcing the browser to download a full-width image for a card that is 300 px wide. CLS spikes follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loading="eager"&lt;/code&gt; on every image.&lt;/strong&gt; This is usually a developer who "fixed" LCP on the hero by removing &lt;code&gt;loading="lazy"&lt;/code&gt; globally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bundle analysis.&lt;/strong&gt; Ask: "How big is your largest JS chunk and what is in it?" If they have never run &lt;code&gt;@next/bundle-analyzer&lt;/code&gt; on a client project, they are shipping blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route handlers used where Server Actions would be simpler.&lt;/strong&gt; Not a hard rule, but it signals they are pattern-matching from Pages Router API routes rather than thinking in RSC architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rate benchmarks for 2026
&lt;/h2&gt;

&lt;p&gt;Rates vary by region, experience tier, and engagement type. These are honest benchmarks from what I see in the market.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Freelance day rate (USD)&lt;/th&gt;
&lt;th&gt;Monthly retainer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Junior (1–2 years, Pages Router era)&lt;/td&gt;
&lt;td&gt;$200–$350&lt;/td&gt;
&lt;td&gt;$2,500–$4,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid (2–4 years, App Router production exp.)&lt;/td&gt;
&lt;td&gt;$400–$600&lt;/td&gt;
&lt;td&gt;$5,000–$8,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senior (4+ years, performance + CMS depth)&lt;/td&gt;
&lt;td&gt;$650–$1,000&lt;/td&gt;
&lt;td&gt;$9,000–$14,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency team (design + dev + PM)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$12,000–$30,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;India-based freelancers typically run 40–60% of the above. That gap is real and the quality range is also wide — the vetting process in this post matters more at that price point, not less, because there are more candidates.&lt;/p&gt;

&lt;p&gt;If someone quotes significantly below the junior band and claims senior-level output, ask for a paid two-hour technical task before committing to a project engagement. A small paid test is normal, protects both sides, and tells you more than any interview.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to send them before the first call
&lt;/h2&gt;

&lt;p&gt;Send a one-paragraph brief: what the site does, rough page count, CMS preference if any, and whether you have an existing codebase or are starting fresh. Then ask them to respond with: their relevant past project (one sentence each), their preferred rendering strategy for this use case and why, and their availability.&lt;/p&gt;

&lt;p&gt;The quality of that response — not whether they use the right buzzwords, but whether they ask a clarifying question about your data model or traffic patterns — is the best pre-screening signal you have. Developers who have shipped real projects ask questions. Developers who are guessing give you a generic pitch.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>approuter</category>
      <category>corewebvitals</category>
      <category>performance</category>
    </item>
    <item>
      <title>Node.js CMS in 2026: Payload, Strapi, Directus, Keystone, and Sanity compared</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Tue, 04 Aug 2026 07:07:43 +0000</pubDate>
      <link>https://dev.to/nayankyada/nodejs-cms-in-2026-payload-strapi-directus-keystone-and-sanity-compared-1di</link>
      <guid>https://dev.to/nayankyada/nodejs-cms-in-2026-payload-strapi-directus-keystone-and-sanity-compared-1di</guid>
      <description>&lt;p&gt;If you're building a custom website on Node.js and need a CMS to match, five names keep coming up: Payload, Strapi, Directus, Keystone, and Sanity. They're all JavaScript-native, all TypeScript-friendly, and all capable of feeding a Next.js frontend — but they make very different bets on where complexity lives and who pays for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Node.js CMS" actually means here
&lt;/h2&gt;

&lt;p&gt;I'm using the term loosely to mean: a CMS whose core runtime is Node.js, whose config or schema is written in JS/TS, and which a JavaScript developer can extend without leaving the language. That rules out Contentful, Storyblok, and Hygraph (all closed-source SaaS platforms with no meaningful Node.js extension layer). It includes Sanity even though the content lake is hosted — the Studio, the schema, the GROQ client, and the document actions are all TypeScript running in Node.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five options 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;&lt;/th&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Strapi&lt;/th&gt;
&lt;th&gt;Directus&lt;/th&gt;
&lt;th&gt;Keystone&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;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;SEE / Community&lt;/td&gt;
&lt;td&gt;BSL 1.1&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-host option&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (required on free)&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;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Payload Cloud ($25+/mo)&lt;/td&gt;
&lt;td&gt;Strapi Cloud ($29+/mo)&lt;/td&gt;
&lt;td&gt;Directus Cloud ($15+/mo)&lt;/td&gt;
&lt;td&gt;No official cloud&lt;/td&gt;
&lt;td&gt;Sanity Cloud (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript config&lt;/td&gt;
&lt;td&gt;JS/TS files or GUI&lt;/td&gt;
&lt;td&gt;Admin GUI + YAML/JSON&lt;/td&gt;
&lt;td&gt;TypeScript config&lt;/td&gt;
&lt;td&gt;TypeScript config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres / MongoDB / SQLite&lt;/td&gt;
&lt;td&gt;Postgres / MySQL / SQLite&lt;/td&gt;
&lt;td&gt;Postgres / MySQL / SQLite / Mongo&lt;/td&gt;
&lt;td&gt;Postgres / SQLite&lt;/td&gt;
&lt;td&gt;Managed (proprietary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REST + GraphQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;REST only (GraphQL plugin)&lt;/td&gt;
&lt;td&gt;GraphQL only&lt;/td&gt;
&lt;td&gt;GROQ + REST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real-time / live preview&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via Next.js draft mode&lt;/td&gt;
&lt;td&gt;Via Strapi preview&lt;/td&gt;
&lt;td&gt;Via Directus Realtime&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Sanity Presentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic transforms&lt;/td&gt;
&lt;td&gt;Cloudinary/custom plugin&lt;/td&gt;
&lt;td&gt;Built-in transforms&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Sanity CDN + hotspot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript depth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep (schema → types)&lt;/td&gt;
&lt;td&gt;Shallow&lt;/td&gt;
&lt;td&gt;Shallow&lt;/td&gt;
&lt;td&gt;Deep (schema → types)&lt;/td&gt;
&lt;td&gt;Deep (TypeGen)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rough infra cost (small project)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0 self-host + VPS ~$12/mo&lt;/td&gt;
&lt;td&gt;$0 self-host + VPS ~$12/mo&lt;/td&gt;
&lt;td&gt;$0 self-host + VPS ~$12/mo&lt;/td&gt;
&lt;td&gt;$0 self-host + VPS ~$12/mo&lt;/td&gt;
&lt;td&gt;$0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Payload CMS
&lt;/h2&gt;

&lt;p&gt;Payload v3 is my current default recommendation for teams that want full ownership and are comfortable writing schema as TypeScript. The config file is pure TS, collections generate types automatically, and the v3 integration with Next.js App Router means you can run the CMS and the frontend in the same Next.js project — no separate server.&lt;/p&gt;

&lt;p&gt;The trade-off is operational. You own the database, the backups, the upgrades. On a $12/mo VPS that's fine for a small marketing site. For a high-availability production app you need managed Postgres, proper backups, and monitoring — suddenly "free" costs real engineering time. Payload Cloud starts at $25/mo and handles that for you, but it's still early and doesn't have the track record of a fully managed SaaS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// payload.config.ts — minimal collection with typed fields&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;buildConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload/config&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;postgresAdapter&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;@payloadcms/db-postgres&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;buildConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;postgresAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;connectionString&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;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;richText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;publishedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strapi
&lt;/h2&gt;

&lt;p&gt;Strapi is the most widely deployed Node.js CMS by install count. The GUI-based content type builder lowers the entry bar for non-developers, which is genuinely useful when a client needs to add a field without filing a ticket.&lt;/p&gt;

&lt;p&gt;The problems I've hit in production: the TypeScript support is bolted on rather than intrinsic (types are generated, not inferred from config), the plugin ecosystem is fragile across major versions, and the BSL-adjacent licensing on Strapi v5's enterprise features can catch teams off guard. For a Next.js project where a developer controls the schema anyway, I don't reach for Strapi first — the GUI builder buys nothing and the type safety gap is annoying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directus
&lt;/h2&gt;

&lt;p&gt;Directus takes a different angle: it wraps an existing database and generates a REST API and admin UI from the schema rather than owning the schema itself. That's genuinely powerful for teams migrating an existing Postgres database or building data-heavy internal tools. The Flows feature (visual automation) is also surprisingly capable.&lt;/p&gt;

&lt;p&gt;For a greenfield content website it's overkill. The BSL 1.1 license also means you can't offer Directus as a managed service to clients without a commercial agreement. Worth knowing before you propose it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keystone
&lt;/h2&gt;

&lt;p&gt;Keystone is the underdog here and it deserves more attention in TypeScript-first teams. The schema is pure TypeScript with deep GraphQL integration — you define a list, Keystone generates the GraphQL schema, the Admin UI, and the TypeScript types in one step. It's maintained by Thinkmill and has been production-stable for years.&lt;/p&gt;

&lt;p&gt;The gaps: no official managed cloud, image handling is DIY, and the community is small compared to Strapi or Payload. I'd reach for Keystone on an internal tool or a project where GraphQL is a hard requirement and the team is comfortable managing their own infra.&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;// keystone.ts — Post list with auto-generated types and GraphQL&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;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;list&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;@keystone-6/core&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;select&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;@keystone-6/core/fields&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;config&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgresql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity is the outlier in this list because you can't self-host the content lake. The Studio and schema are Node.js/TypeScript, but the data lives on Sanity's infrastructure. For some teams that's disqualifying; for most product and agency teams it's the right trade-off — you get a fast global CDN, real-time collaboration, Presentation (live preview), image transforms with hotspot, and a free tier that covers most small projects.&lt;/p&gt;

&lt;p&gt;Typegen generates TypeScript interfaces directly from GROQ queries, which is the tightest end-to-end type safety of any option here. The free tier (up to 2 users, 200k API requests/month, 10 GB bandwidth) is genuinely usable in production for a marketing site. Growth plan starts at $15/seat/month.&lt;/p&gt;

&lt;p&gt;The constraint worth calling out: if you need custom server-side business logic (e.g., a webhook that writes back to the CMS on an order event), you're adding a separate API layer. With Payload or Keystone that logic lives inside the CMS config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Payload&lt;/strong&gt; if you want full ownership, are running Next.js App Router, and your team is comfortable with TypeScript config and managing a database. Best self-hosted Node.js CMS right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Strapi&lt;/strong&gt; if a non-developer needs to manage the schema through a GUI and you're willing to accept looser TypeScript coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Directus&lt;/strong&gt; if you're wrapping an existing database or building a data-heavy internal dashboard, not a content-first website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Keystone&lt;/strong&gt; if GraphQL is a hard requirement and you want the cleanest TypeScript-to-schema mapping with full infrastructure ownership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Sanity&lt;/strong&gt; if you want zero infrastructure management, real-time collaboration, best-in-class image delivery, and you're comfortable trading self-host control for a managed SaaS. It's what I use on most client projects for exactly those reasons.&lt;/p&gt;

&lt;p&gt;The honest answer: for a custom Next.js marketing site in 2026, it's almost always Payload or Sanity. Payload when the client needs to own every byte. Sanity when speed of delivery and editorial experience matter more than infrastructure control.&lt;/p&gt;

</description>
      <category>headlesscms</category>
      <category>comparison</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Next.js + React stack explained: what each layer does and when it's wrong</title>
      <dc:creator>Nayan Kyada</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:55:31 +0000</pubDate>
      <link>https://dev.to/nayankyada/the-nextjs-react-stack-explained-what-each-layer-does-and-when-its-wrong-2m4a</link>
      <guid>https://dev.to/nayankyada/the-nextjs-react-stack-explained-what-each-layer-does-and-when-its-wrong-2m4a</guid>
      <description>&lt;p&gt;The Next React stack is what most product teams land on after trying a few alternatives — React for the UI layer, Next.js as the framework, a headless CMS like Sanity for content, Tailwind for styling, and Vercel or a similar platform for deployment. Each of those choices is independent, but they've converged into a default for a reason. This guide explains what each layer actually does, why the combination became dominant, and — just as importantly — the situations where this stack is the wrong call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "the Next React stack" actually means
&lt;/h2&gt;

&lt;p&gt;People use "Next.js stack" and "React stack" interchangeably, but there's a useful distinction. React is a UI library — it renders components, manages local state, and handles events in the browser. It has no opinion about routing, data fetching, or how you ship to production. Next.js is the opinionated framework built on top of React that fills those gaps: file-system routing, server components, ISR, image optimisation, metadata API, and so on.&lt;/p&gt;

&lt;p&gt;So when a team says "we're on Next React", they usually mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19&lt;/strong&gt; — the UI primitives (hooks, server components, Suspense boundaries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js App Router&lt;/strong&gt; — routing, rendering strategy, API/route handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A headless CMS&lt;/strong&gt; — Sanity, Contentful, or Payload for structured content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — utility-first styling (v4 as of 2026)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel or Cloudflare&lt;/strong&gt; — edge deployment, CDN, and preview environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those five layers cover the vast majority of what a product or marketing site needs. The rest — search, email, video, auth — gets bolted on as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  React: the UI layer
&lt;/h2&gt;

&lt;p&gt;React's job is to describe what the screen looks like as a function of data. With React 19 and the App Router, a large portion of that work now happens on the server. React Server Components (RSC) fetch data and render HTML without sending any JavaScript to the browser — which is a meaningful shift from the client-heavy SPAs React was associated with before 2023.&lt;/p&gt;

&lt;p&gt;The practical result: a product page that queries your CMS and renders a list of articles ships zero JavaScript for the article list itself. Only the parts that need interactivity — a search input, a like button — stay as client components.&lt;/p&gt;

&lt;p&gt;This isn't free complexity reduction. RSC introduces a clear mental model split: server components cannot use browser APIs, hooks, or event handlers. Client components must be explicitly marked with &lt;code&gt;'use client'&lt;/code&gt;. Teams unfamiliar with this boundary tend to scatter &lt;code&gt;'use client'&lt;/code&gt; everywhere, negating the performance benefit. Getting comfortable with that boundary is the main learning curve in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js: the framework layer
&lt;/h2&gt;

&lt;p&gt;Next.js sits above React and makes decisions you'd otherwise make yourself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing&lt;/strong&gt; — The App Router maps the &lt;code&gt;app/&lt;/code&gt; directory to URL segments. Dynamic routes (&lt;code&gt;app/posts/[slug]/page.tsx&lt;/code&gt;), catch-all routes, route groups, parallel routes, and intercepted routes cover nearly every URL shape without a separate router library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering strategy&lt;/strong&gt; — Each page or segment can be statically generated, server-rendered per request, or incrementally revalidated. With Partial Prerendering (PPR), you can prerender the shell and stream dynamic parts — so a blog post's static body serves from the edge CDN while the dynamic comment count streams in separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image and font&lt;/strong&gt; — &lt;code&gt;next/image&lt;/code&gt; handles format negotiation (WebP/AVIF), lazy loading, and CLS prevention via reserved space. &lt;code&gt;next/font&lt;/code&gt; subsetting eliminates render-blocking font requests. Both are opt-in but the defaults are good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata API&lt;/strong&gt; — The &lt;code&gt;generateMetadata&lt;/code&gt; function in each route segment lets you return &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, Open Graph tags, and canonical URLs with access to route params and async data — so page titles and descriptions come from your CMS without a third-party library.&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/posts/[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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;description&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; slug.current == $slug][0]{ title, description }`&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="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;description&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="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="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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 }`&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="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;h2&gt;
  
  
  The CMS layer
&lt;/h2&gt;

&lt;p&gt;Next.js doesn't care where your data comes from, but most teams need structured content — blog posts, product pages, landing page blocks — managed by non-developers. That's where a headless CMS fits in.&lt;/p&gt;

&lt;p&gt;Sanity is the most common pairing in this stack because it exposes a real-time content API with typed GROQ queries, stores images on a CDN with on-the-fly transforms, and runs Sanity Studio (the editor UI) either at a separate URL or embedded at &lt;code&gt;/studio&lt;/code&gt; inside your Next.js app. The schema lives in code, not in a GUI, which means it's version-controlled alongside your components.&lt;/p&gt;

&lt;p&gt;Contentful and Payload are viable alternatives depending on budget and self-hosting preference — but the integration pattern with Next.js is roughly the same: query the CMS in a server component, pass typed props to client components, revalidate on webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling: Tailwind CSS v4
&lt;/h2&gt;

&lt;p&gt;Tailwind v4 dropped the &lt;code&gt;tailwind.config.js&lt;/code&gt; file in favour of CSS-native configuration via &lt;code&gt;@theme&lt;/code&gt; in a root CSS file. In a Next.js project, that means adding one import to &lt;code&gt;app/globals.css&lt;/code&gt; and writing utility classes the same way you always did — no breaking change to the component API, just a faster build.&lt;/p&gt;

&lt;p&gt;The reason Tailwind became the default in this stack: it eliminates the runtime cost of CSS-in-JS (no hydration penalty, no style injection on the client), produces near-zero unused CSS in production thanks to the scanner, and keeps styling colocated with markup without a separate &lt;code&gt;.module.css&lt;/code&gt; file per component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting: Vercel vs the alternatives
&lt;/h2&gt;

&lt;p&gt;Vercel is the canonical deployment platform for Next.js — it was built by the same team, and features like ISR, PPR, edge middleware, and image optimisation are tested there first. For most teams, the zero-configuration deploy from a GitHub push, plus per-branch preview URLs, is worth the cost until scale makes the per-seat pricing awkward (the hobby-to-pro gap is around $20/month; team pricing scales from there).&lt;/p&gt;

&lt;p&gt;Cloudflare Pages supports Next.js with the &lt;code&gt;@cloudflare/next-on-pages&lt;/code&gt; adapter. It's cheaper at scale and has better global edge coverage, but some App Router features — specifically certain streaming and middleware behaviours — have lagged behind Vercel's support. AWS Amplify Gen 2 and Railway are options for teams that need VPC-adjacent deployment or want to avoid Vercel lock-in entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this combination won
&lt;/h2&gt;

&lt;p&gt;The short answer: each layer solved a real problem that its predecessor left open.&lt;/p&gt;

&lt;p&gt;Create React App gave you React without a build opinion, but nothing else — no SSR, no routing, no image handling. Gatsby added static generation but required a GraphQL layer for everything, which felt overengineered for most content sites. Next.js Pages Router fixed Gatsby's data fetching complexity while keeping SSR an option. The App Router completed the picture by making server components the default, which finally made the "zero JS for static content" promise practical rather than theoretical.&lt;/p&gt;

&lt;p&gt;Tailwind replaced CSS-in-JS libraries that were introducing hydration overhead and unpredictable specificity. Sanity replaced WordPress for teams that needed structured content without PHP. Vercel replaced DigitalOcean droplets for teams that didn't want to manage Nginx configs.&lt;/p&gt;

&lt;p&gt;Each swap was motivated by a concrete pain point, not trend-chasing. That's why the stack has staying power.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this stack is the wrong choice
&lt;/h2&gt;

&lt;p&gt;The Next React stack is not universally correct. Three scenarios where you should think twice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mostly static, no interactivity.&lt;/strong&gt; If you're shipping a documentation site or a blog with no dynamic personalisation, no real-time content, and no complex UI, Astro will produce smaller bundles and simpler deployment with less configuration. Next.js adds framework overhead that documentation sites don't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highly interactive, app-like UI.&lt;/strong&gt; If your product is closer to Figma than to a marketing site — lots of canvas interaction, real-time collaboration, drag-and-drop at scale — React's concurrent model is still the right choice but you may not need Next.js's server rendering machinery at all. A Vite-built SPA with a dedicated API backend is simpler to reason about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very small teams with no React experience.&lt;/strong&gt; The App Router's RSC model has a non-trivial learning curve. If your team is two developers who know Vue well and need to ship in six weeks, adopting Next.js will slow you down. The stack rewards teams who already know React; it taxes teams who don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the layers together
&lt;/h2&gt;

&lt;p&gt;A minimal Next React stack project structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  layout.tsx          # root layout, fonts, global CSS
  page.tsx            # home page — server component
  posts/
    [slug]/
      page.tsx        # post detail — server component + generateMetadata
sanity/
  lib/client.ts       # Sanity client config
  schemas/            # document type schemas
components/
  ui/                 # Radix-based primitives
  blocks/             # CMS-driven page sections
public/
tailwind.css          # @import "tailwindcss"; @theme { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;app/&lt;/code&gt; tree handles routing and rendering. The &lt;code&gt;sanity/&lt;/code&gt; tree owns the CMS integration — client, schemas, and GROQ query helpers. Components are split between generic UI primitives (usually Radix-wrapped) and CMS-driven blocks that map a content type to a layout.&lt;/p&gt;

&lt;p&gt;This separation isn't arbitrary. When a designer changes a component, they touch &lt;code&gt;components/ui&lt;/code&gt;. When an editor adds a new content block type, the schema and the block component change together, nothing else. That's the modularity the stack is designed to produce — and it holds as long as each layer stays in its lane.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>approuter</category>
      <category>headlesscms</category>
    </item>
  </channel>
</rss>
