<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sanity</title>
<link>https://coderlegion.com/tag/sanity</link>
<description>Powered by Question2Answer</description>
<item>
<title>Is there any way to make type conversion sexy? Nope</title>
<link>https://coderlegion.com/31882/is-there-any-way-to-make-type-conversion-sexy-nope</link>
<description>&lt;p&gt;When converting a value of one type to another; There are conversions which never fail (ie int -&amp;gt; string). Some that can fail like string -&amp;gt; int, and then some lossy and implicit ones ( number -&amp;gt; int, int -&amp;gt; number)&lt;/p&gt;
&lt;p&gt;How do we spell it in any of those situations?&lt;/p&gt;
&lt;p&gt;My lean is to have a single tool with sane defaults. if you don't cover the error arm it wont compile and tell you why. That would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;typed ${num} chars&amp;lt;/&amp;gt; // No explicit cast needed, behaviour is dictated by the &amp;lt;p&amp;gt; parent

 &amp;lt;age:int | not=(int.parse(@txt))/&amp;gt; // &quot;42&quot; → 42, &quot;abc&quot; → not

type Guess:enum = { Yes, No }

&amp;lt;g:Guess | not=(Guess.parse(@txt))/&amp;gt;     // &quot;Yes&quot; → .Yes, &quot;yes&quot; → not

&amp;lt;p&amp;gt;${match @g {
    .Yes :&amp;gt; &quot;yeppers&quot;,
    .No :&amp;gt; &quot;nope&quot;,
    not :&amp;gt; &quot;not a guess&quot;,
}}&amp;lt;/p&amp;gt;

function qty(s: string) -&amp;gt; int {
    const n = int.parse(s)
    if (n is not) {
        return 0
    }
    return n                  // narrowed to int here
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lossy conversions are named, and round needs a tie mode&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const avg = number.parse(@txt)          // number | not
// once narrowed:
floor(avg)                 // 3.7 → 3
ceil(avg)                  // 3.2 → 4
trunc(avg)                 // -3.7 → -3
round(avg, .HalfUp)        // 2.5 → 3
round(avg, .HalfEven)      // 2.5 → 2
round(avg)                 // error: name a tie mode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Into text: no conversion spelled, except where it's refused&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;typed ${@txt.length} chars&amp;lt;/p&amp;gt;          // ok: markup converts primitives
const msg = `typed ${@txt.length} chars`   // ok: a template is a text context
&quot;typed &quot; + @txt.length                     // error: + stays strict
label(@count)                              // error: int is not string
label(`${@count}`)                         // ok
&amp;lt;p&amp;gt;${@user}&amp;lt;/p&amp;gt;                            // error: a struct has no one honest text form
fixed(@price, 2)                           // &quot;3.14&quot;, from scrml:format
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bound number cell turns bad input into not&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let &amp;lt;qty:int | not=0/&amp;gt;
&amp;lt;input type=&quot;text&quot; bind:value=@qty/&amp;gt;      // &quot;abc&quot; writes not, never 0 or NaN


int.parse(&quot;42&quot;)       // 42
int.parse(&quot;-7&quot;)       // -7
int.parse(&quot; 42&quot;)      // not   (no trimming)
int.parse(&quot;+42&quot;)      // not   (no plus sign)
int.parse(&quot;12px&quot;)     // not   (whole string or nothing)
int.parse(&quot;0x1F&quot;)     // not   (use scrml:math.parseInt(s, 16) for a radix)
int.parse(&quot;3.5&quot;)      // not   (tnumber, not an int)

number.parse(&quot;3.5&quot;)   // 3.5
number.parse(&quot;1e3&quot;)   // 1000
number.parse(&quot;.5&quot;)    // decided in the SPEC text; strict leans not

bool.parse(&quot;true&quot;)    // true
bool.parse(&quot;True&quot;)    // not
bool.parse(&quot;yes&quot;)     // not
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Would anyone like to offer any criticism to this approach. Maybe a better idea. &lt;/p&gt;
&lt;p&gt;I hope so&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31882/is-there-any-way-to-make-type-conversion-sexy-nope</guid>
<pubDate>Tue, 20 Oct 2026 19:04:00 +0000</pubDate>
</item>
<item>
<title>Ultra Bridge: Translating JAWS Keyboard Commands to NVDA with Sanity Context</title>
<link>https://coderlegion.com/29902/ultra-bridge-translating-jaws-keyboard-commands-to-nvda-with-sanity-context</link>
<description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/challenges/sanity-2026-09-16&quot;&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;Ultra Bridge is an agent for screen reader users. You ask it things like &quot;What is the NVDA equivalent of JAWS Insert+F7?&quot; or &quot;Which keys do different jobs in JAWS and NVDA?&quot;, and it answers from a Sanity dataset through Sanity Context MCP. Every answer says how well the underlying command is verified, so you know whether to trust it or check it yourself first.&lt;/p&gt;
&lt;p&gt;I built it because I rely on a screen reader myself, and moving between JAWS and NVDA means relearning keys that look the same but are not. That gap costs real time and real confidence, and it is exactly the kind of problem structured content is good at closing. Ultra Bridge is part of the Ultra suite of accessible software I am building.&lt;/p&gt;
&lt;p&gt;The reason it needs structured content: a keyword search can find a page that mentions both screen readers, but it cannot tell you which keys collide, which ones translate cleanly, or which ones simply do not exist yet on the other side. In Ultra Bridge an &lt;strong&gt;action&lt;/strong&gt; (&quot;show a list of all links&quot;) is separate from a &lt;strong&gt;command&lt;/strong&gt; (the keys one screen reader uses for that action). Equivalents are found through the shared action, so a relationship question becomes one query instead of a guess.&lt;/p&gt;
&lt;p&gt;This query lists every single letter key that exists in both screen readers but does a different job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groq&quot;&gt;*[_type==&quot;command&quot; &amp;amp;&amp;amp; screenReader-&amp;gt;slug.current==&quot;jaws&quot; &amp;amp;&amp;amp; context==&quot;browse-mode&quot;]{
  keys,
  &quot;jawsAction&quot;: action-&amp;gt;title,
  &quot;nvdaMeansSomethingElse&quot;: *[_type==&quot;command&quot; &amp;amp;&amp;amp; screenReader-&amp;gt;slug.current==&quot;nvda&quot; &amp;amp;&amp;amp; keys==^.keys &amp;amp;&amp;amp; action._ref != ^.action._ref]{&quot;action&quot;: action-&amp;gt;title}
}[count(nvdaMeansSomethingElse)&amp;gt;0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Against the seed data it returns six keys. R is &quot;region&quot; in JAWS and &quot;radio button&quot; in NVDA. Q is &quot;main content&quot; in JAWS and &quot;block quote&quot; in NVDA. A, O, S and D collide too. That is exactly the kind of thing that costs a person time when they switch, and exactly the kind of question a keyword search cannot answer.&lt;/p&gt;
&lt;h3 id=&quot;demo&quot;&gt;Demo&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ultra-bridge-xm71.vercel.app/&quot;&gt;https://ultra-bridge-xm71.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The page has example questions you can press. Open &quot;How this was found&quot; under any answer to see the exact GROQ query the agent ran to get there — nothing is hidden between the question and the data.&lt;/p&gt;
&lt;h3 id=&quot;code&quot;&gt;Code&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/demirajvazi10-max/Ultra-Bridge&quot;&gt;https://github.com/demirajvazi10-max/Ultra-Bridge&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-i-used-sanity&quot;&gt;How I Used Sanity&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Schema.&lt;/strong&gt; Seven document types: &lt;code&gt;screenReader&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;concept&lt;/code&gt;, &lt;code&gt;difference&lt;/code&gt; and &lt;code&gt;guide&lt;/code&gt;. The dataset currently has 229 documents: 60 actions, 127 commands, 13 concepts, 12 differences, 7 guides and 8 sources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trust as data, not as an afterthought.&lt;/strong&gt; Every command has a &lt;code&gt;verification&lt;/code&gt; field (&lt;code&gt;tested-by-author&lt;/code&gt;, &lt;code&gt;cross-checked&lt;/code&gt;, &lt;code&gt;first-party&lt;/code&gt;, &lt;code&gt;third-party&lt;/code&gt;, &lt;code&gt;recalled&lt;/code&gt;) and references to &lt;code&gt;source&lt;/code&gt; documents. The system prompt tells the agent to say the verification level out loud whenever it uses a command, and to point to input help (Insert+1 in JAWS, NVDA+1 in NVDA) whenever a command is not yet confirmed. When two sources disagree, the disagreement is written on the command instead of hidden. Right now 11 commands are tested-by-author, 58 cross-checked, 44 first-party and 14 third-party — zero are still sitting at the weakest &quot;recalled&quot; level, because I keep a running checklist of what still needs a hands-on pass on a real screen reader.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sanity Context, two modes.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GROQ mode serves the structured dataset. The agent fetches &lt;code&gt;/initial-context&lt;/code&gt; over HTTP, puts it in its system prompt, and uses &lt;code&gt;groq_query&lt;/code&gt; and &lt;code&gt;schema_explorer&lt;/code&gt; for anything that names a specific command, key or action.&lt;/li&gt;
&lt;li&gt;Knowledge Base mode serves seven guides written in prose, reserved for open-ended questions like &quot;what should I learn first&quot; or &quot;how are the two screen readers different overall,&quot; where a single structured query would not be the right shape of answer. The Knowledge Base is created and built from the CLI (&lt;code&gt;sanity context create&lt;/code&gt;, &lt;code&gt;imports create&lt;/code&gt;, &lt;code&gt;build --watch&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The two &lt;code&gt;initial_context&lt;/code&gt; tools have the same name, so the agent drops both and inlines their content directly into the system prompt instead. If the Knowledge Base endpoint is unavailable, the agent falls back to the structured dataset only rather than failing the whole request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Engineering for free-tier reality.&lt;/strong&gt; The public demo runs on free model tiers, which cap tokens per minute hard enough that a naive integration falls over under real traffic. So the agent only exposes the two tools it actually needs (&lt;code&gt;groq_query&lt;/code&gt;, &lt;code&gt;knowledge_base_read&lt;/code&gt;), compacts every tool result before it goes back to the model (stripping fields the model does not need and hard-capping length), and chains four model providers (Google, Groq, Cerebras, Anthropic) so a rate limit on one provider fails over to the next instead of failing the request. The public page also rate-limits per visitor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I learned.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One endpoint serves one source type, so the structured data and the Knowledge Base need separate access paths.&lt;/li&gt;
&lt;li&gt;GROQ &lt;code&gt;match&lt;/code&gt; is tokenised text matching, so I use equality (&lt;code&gt;==&lt;/code&gt;) for keys that contain &lt;code&gt;+&lt;/code&gt;, or the match would silently split &quot;Insert+F7&quot; into pieces.&lt;/li&gt;
&lt;li&gt;The Sanity CLI covers almost everything I needed: project, schema deploy, dataset import, tokens and the Knowledge Base. That mattered because I work from the command line.&lt;/li&gt;
&lt;li&gt;Because Sanity's API was not reachable from where I built the first version, I wrote a small local server that speaks the Context MCP tool names and evaluates real GROQ with &lt;code&gt;groq-js&lt;/code&gt;. It let me test the agent, the prompt queries and the whole interface offline, and it ships in the repo so anyone can try the project without accounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Accessibility.&lt;/strong&gt; I am blind and use JAWS day to day, and I tested this myself before anyone else touched it. The interface has a skip link, real headings for every question and answer, one polite live region that announces the full answer once when it is complete, a &quot;read last answer again&quot; button, and stable focus in the question box. An end-to-end test drives it in Chromium and runs axe-core in light and dark colour schemes, so accessibility is checked automatically, not just by hand.&lt;/p&gt;
&lt;h3 id=&quot;sanity-project-details&quot;&gt;Sanity Project Details&lt;/h3&gt;
&lt;p&gt;Project ID: u2f7mbu5&lt;br&gt;
Dataset: production (public)&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29902/ultra-bridge-translating-jaws-keyboard-commands-to-nvda-with-sanity-context</guid>
<pubDate>Mon, 05 Oct 2026 18:59:50 +0000</pubDate>
</item>
<item>
<title>How to Integrate Sanity CMS with Next.js 16 App Router</title>
<link>https://coderlegion.com/23495/how-to-integrate-sanity-cms-with-next-js-16-app-router</link>
<description>&lt;p&gt;Managing content as a developer used to mean one of two things: hardcoding everything into your codebase or wrestling with a clunky WordPress dashboard. Neither feels great when your client wants to update a blog post at 10pm on a Friday without touching a single line of code.&lt;/p&gt;
&lt;p&gt;That's exactly where &lt;strong&gt;Sanity CMS&lt;/strong&gt; shines. It's a headless CMS with a fully customizable studio, a powerful query language called GROQ, and first-class support for Next.js. And in 2026, with Next.js 16's App Router fully matured, the two work together beautifully.&lt;/p&gt;
&lt;p&gt;In this guide, you'll set up Sanity CMS inside a Next.js 16 App Router project from scratch: creating a project, defining a schema, writing GROQ queries, typing everything with TypeScript, and rendering rich text content with live updates. By the end, you'll have a working blog listing page and a dynamic post page, both powered entirely by Sanity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-sanity-over-other-headless-cms-options-&quot;&gt;Why Sanity Over Other Headless CMS Options?&lt;/h3&gt;
&lt;p&gt;There are plenty of headless CMS choices out there — Contentful, Strapi, Payload — so why Sanity?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sanity Studio runs inside your Next.js app&lt;/strong&gt; at &lt;code&gt;/studio&lt;/code&gt;, so there's no separate admin URL to manage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GROQ&lt;/strong&gt; (Graph-Relational Object Queries) is more flexible and readable than GraphQL for content queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time collaboration&lt;/strong&gt; — multiple editors can work on content simultaneously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free tier is genuinely generous&lt;/strong&gt; for most personal projects and small clients.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building&quot;&gt;What We're Building&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;An embedded Sanity Studio running at &lt;code&gt;/studio&lt;/code&gt; inside your Next.js app&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;post&lt;/code&gt; schema with title, slug, excerpt, cover image, and rich text body&lt;/li&gt;
&lt;li&gt;GROQ queries to fetch all posts and a single post by slug&lt;/li&gt;
&lt;li&gt;Fully typed Sanity responses with TypeScript&lt;/li&gt;
&lt;li&gt;A blog listing page and a dynamic post page using &lt;code&gt;generateStaticParams&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Real-time content updates using Sanity Live&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-sanity-exactly-&quot;&gt;What Is Sanity, Exactly?&lt;/h3&gt;
&lt;p&gt;Sanity is a &lt;strong&gt;headless CMS&lt;/strong&gt; — meaning the content editor (Studio) is completely decoupled from how your content gets displayed. You configure Studio with code, content gets stored remotely in what Sanity calls the Content Lake, and your Next.js app queries that content using GROQ (Sanity's own query language).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Think of Sanity Studio as a &quot;window&quot; into your remote content. The Studio configuration lives in your repo, but the actual blog posts, images, and data live on Sanity's servers — not in your codebase.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js 20+&lt;/strong&gt; installed&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Next.js 16&lt;/strong&gt; project with the App Router (TypeScript + Tailwind)&lt;/li&gt;
&lt;li&gt;A free account at &lt;a rel=&quot;nofollow&quot; href=&quot;https://sanity.io&quot;&gt;sanity.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;h3 id=&quot;basic-familiarity-with-next-js-file-based-routing&quot;&gt;Basic familiarity with Next.js file-based routing&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-1-create-a-sanity-project&quot;&gt;Step 1: Create a Sanity Project&lt;/h3&gt;
&lt;p&gt;First, let's create a new Sanity project from the CLI. If you don't have the Sanity CLI installed yet, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @sanity/cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then inside your &lt;strong&gt;Next.js project root&lt;/strong&gt;, initialize Sanity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx sanity@latest init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI will walk you through a series of prompts. Here's exactly what to choose:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;✔ Create a new project or select an existing one → Create new project
✔ Project name → whatever you like (e.g. my-blog)
✔ Select organization or create new
✔ Use the default dataset configuration? → Yes
✔ Configure Sanity MCP and agent skills for these editors? → Claude Code, GitHub Copilot CLI, VS Code
✔ Would you like to add configuration files for a Sanity project in this Next.js folder? → Yes
✔ Do you want to use TypeScript? → Yes
✔ Would you like an embedded Sanity Studio? → Yes
✔ What route do you want to use for the Studio? → /studio
✔ Select project template to use → Clean project with no predefined schema types
✔ Would you like to add the project ID and dataset to your .env.local file? → Yes
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Picking the &quot;blog&quot; template instead of &quot;Clean project&quot; gives you pre-built schema types (&lt;code&gt;post&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt;). That's fine if you want a head start, but this guide builds the &lt;code&gt;post&lt;/code&gt; schema manually so you understand exactly what each field does — so we go with the clean template.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once it finishes, your project folder should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;.
├── .env.local
├── sanity.cli.ts
├── sanity.config.ts
├── (...and all your Next.js files)
└── src
    ├── app
    │   └── studio
    │       └── [[...tool]]
    │           └── page.tsx
    └── sanity
        ├── lib
        │   ├── client.ts
        │   ├── image.ts
        │   └── live.ts
        ├── schemaTypes
        │   └── index.ts
        ├── env.ts
        └── schema.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-just-got-created-&quot;&gt;What Just Got Created?&lt;/h4&gt;
&lt;p&gt;A few of these files matter more than others:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/sanity/lib/client.ts&lt;/code&gt; — a configured Sanity Client instance used to query content&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/sanity/lib/live.ts&lt;/code&gt; — exports &lt;code&gt;sanityFetch&lt;/code&gt; and &lt;code&gt;SanityLive&lt;/code&gt;, which power real-time content updates&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/sanity/env.ts&lt;/code&gt; — reads your project ID and dataset from &lt;code&gt;.env.local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sanity.config.ts&lt;/code&gt; — configures Studio itself: schema types, plugins, and the studio route&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app/studio/[[...tool]]/page.tsx&lt;/code&gt; — the catch-all route that renders the entire Studio UI at &lt;code&gt;/studio&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your &lt;code&gt;.env.local&lt;/code&gt; should now contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NEXT_PUBLIC_SANITY_PROJECT_ID=&quot;your-project-id&quot;
NEXT_PUBLIC_SANITY_DATASET=&quot;production&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; because a project ID and dataset name aren't sensitive — they're needed by the browser to talk to Sanity's API directly when previewing content.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-install-next-sanity&quot;&gt;Step 2: Install next-sanity&lt;/h3&gt;
&lt;p&gt;The CLI usually installs this for you, but if it's missing, add it manually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install next-sanity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;next-sanity&lt;/code&gt; is a toolkit of utilities built specifically for Next.js — it wraps Sanity Client with conventions for data fetching, caching, and live content updates so you're not wiring all of that together yourself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-create-a-post-schema&quot;&gt;Step 3: Create a Post Schema&lt;/h3&gt;
&lt;p&gt;Schemas in Sanity define what content editors are &lt;em&gt;allowed&lt;/em&gt; to create in Studio — think of them as the shape of your content, similar to a database model.&lt;/p&gt;
&lt;p&gt;Create a new schema file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/sanity/schemaTypes/post.ts
import { defineField, defineType } from &quot;sanity&quot;;

export const postSchema = defineType({
  name: &quot;post&quot;,
  title: &quot;Blog Post&quot;,
  type: &quot;document&quot;,
  fields: [
    defineField({
      name: &quot;title&quot;,
      title: &quot;Title&quot;,
      type: &quot;string&quot;,
      validation: (Rule) =&amp;gt; Rule.required(),
    }),
    defineField({
      name: &quot;slug&quot;,
      title: &quot;Slug&quot;,
      type: &quot;slug&quot;,
      options: { source: &quot;title&quot; }, // auto-generates slug from title
      validation: (Rule) =&amp;gt; Rule.required(),
    }),
    defineField({
      name: &quot;excerpt&quot;,
      title: &quot;Excerpt&quot;,
      type: &quot;text&quot;,
      rows: 3,
    }),
    defineField({
      name: &quot;coverImage&quot;,
      title: &quot;Cover Image&quot;,
      type: &quot;image&quot;,
      options: { hotspot: true }, // lets editors set a focal point
    }),
    defineField({
      name: &quot;publishedAt&quot;,
      title: &quot;Published At&quot;,
      type: &quot;datetime&quot;,
    }),
    defineField({
      name: &quot;body&quot;,
      title: &quot;Body&quot;,
      type: &quot;array&quot;,
      of: [{ type: &quot;block&quot; }, { type: &quot;image&quot; }], // rich text + inline images
    }),
  ],
  orderings: [
    {
      title: &quot;Published Date, New&quot;,
      name: &quot;publishedAtDesc&quot;,
      by: [{ field: &quot;publishedAt&quot;, direction: &quot;desc&quot; }],
    },
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick breakdown of what's happening:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;defineType&lt;/code&gt; registers a new document type — editors will see &quot;Blog Post&quot; as a creatable content type in Studio&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defineField&lt;/code&gt; describes each individual field and its validation rules&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;slug&lt;/code&gt; field with &lt;code&gt;options: { source: &quot;title&quot; }&lt;/code&gt; auto-generates a URL-friendly slug from the title as the editor types&lt;/li&gt;
&lt;li&gt;&lt;code&gt;body&lt;/code&gt; uses &lt;code&gt;type: &quot;array&quot;&lt;/code&gt; with &lt;code&gt;block&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; — this is Sanity's &lt;strong&gt;Portable Text&lt;/strong&gt; format, a structured way of representing rich text that isn't tied to HTML&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; &lt;code&gt;Rule.required()&lt;/code&gt; enforces validation inside Studio itself. Editors literally cannot publish a post without a title or slug — no backend validation needed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-register-the-schema&quot;&gt;Step 4: Register the Schema&lt;/h3&gt;
&lt;p&gt;Defining a schema isn't enough — Studio needs to know it exists. Register it in the schema index:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/sanity/schemaTypes/index.ts
import { type SchemaTypeDefinition } from &quot;sanity&quot;;
import { postSchema } from &quot;./post&quot;;

export const schema: { types: SchemaTypeDefinition[] } = {
  types: [postSchema],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting this step is the #1 reason a new schema &quot;doesn't show up&quot; in Studio. Defining the type isn't enough — it has to be added to this &lt;code&gt;types&lt;/code&gt; array.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now start your dev server and visit &lt;code&gt;http://localhost:3000/studio&lt;/code&gt;. You should see &quot;Blog Post&quot; listed as a content type, ready to create your first post.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-write-groq-queries&quot;&gt;Step 5: Write GROQ Queries&lt;/h3&gt;
&lt;p&gt;GROQ (Graph-Relational Object Queries) is Sanity's query language, purpose-built for querying structured, nested JSON-like content.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you've used SQL or MongoDB queries before, GROQ will feel different at first, but it's actually closer to JavaScript object/array syntax than SQL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Create a queries file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/sanity/queries.ts
import { groq } from &quot;next-sanity&quot;;

// Fetch all posts for the blog listing page
export const allPostsQuery = groq`
  *[_type == &quot;post&quot;] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    coverImage {
      asset-&amp;gt; { _id, url }
    }
  }
`;

// Fetch a single post by slug
export const postBySlugQuery = groq`
  *[_type == &quot;post&quot; &amp;amp;&amp;amp; slug.current == $slug][0] {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    coverImage {
      asset-&amp;gt; { _id, url }
    },
    body
  }
`;

// Fetch all slugs for generateStaticParams
export const allPostSlugsQuery = groq`
  *[_type == &quot;post&quot;] { &quot;slug&quot;: slug.current }
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each part of the first query means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*[_type == &quot;post&quot;]&lt;/code&gt; — selects every document where the type equals &lt;code&gt;&quot;post&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;| order(publishedAt desc)&lt;/code&gt; — sorts results by publish date, newest first&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{ ... }&lt;/code&gt; — a projection, meaning &quot;only return these fields,&quot; similar to selecting specific columns in SQL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coverImage { asset-&amp;gt; { _id, url } }&lt;/code&gt; — the &lt;code&gt;-&amp;gt;&lt;/code&gt; is a &lt;strong&gt;dereference&lt;/strong&gt;, following the image asset reference to pull its actual URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second query uses &lt;code&gt;$slug&lt;/code&gt; as a parameter and &lt;code&gt;[0]&lt;/code&gt; to grab just the first matching result, since slugs are unique.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-add-typescript-types&quot;&gt;Step 6: Add TypeScript Types&lt;/h3&gt;
&lt;p&gt;Since GROQ queries return plain JSON, TypeScript has no way to infer their shape automatically. Define the types yourself so the rest of your app stays type-safe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/sanity/types.ts
export interface SanitySlug {
  current: string;
}

export interface SanityImageAsset {
  _id: string;
  url: string;
}

export interface SanityImage {
  asset: SanityImageAsset;
}

export interface Post {
  _id: string;
  title: string;
  slug: SanitySlug;
  excerpt?: string;
  publishedAt: string;
  coverImage?: SanityImage;
  body?: unknown[]; // Portable Text blocks
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Sanity also offers a code generation tool (&lt;code&gt;sanity typegen&lt;/code&gt;) that can generate these types automatically from your schema and queries. Writing them by hand works fine for smaller schemas, but it's worth exploring once your content model grows.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-build-the-blog-listing-page&quot;&gt;Step 7: Build the Blog Listing Page&lt;/h3&gt;
&lt;p&gt;With queries and types ready, fetching content becomes straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/page.tsx
import { sanityFetch } from &quot;@/sanity/lib/live&quot;;
import { allPostsQuery } from &quot;@/sanity/queries&quot;;
import { Post } from &quot;@/sanity/types&quot;;
import Link from &quot;next/link&quot;;

export default async function BlogPage() {
  const { data: posts } = await sanityFetch({ query: allPostsQuery });

  return (
    &amp;lt;main className=&quot;max-w-3xl mx-auto px-4 py-12&quot;&amp;gt;
      &amp;lt;h1 className=&quot;text-4xl font-bold mb-8&quot;&amp;gt;Blog&amp;lt;/h1&amp;gt;
      &amp;lt;ul className=&quot;space-y-8&quot;&amp;gt;
        {(posts as Post[]).map((post) =&amp;gt; (
          &amp;lt;li key={post._id} className=&quot;border-b pb-8&quot;&amp;gt;
            &amp;lt;Link href={`/blog/${post.slug.current}`}&amp;gt;
              &amp;lt;h2 className=&quot;text-2xl font-semibold hover:underline&quot;&amp;gt;
                {post.title}
              &amp;lt;/h2&amp;gt;
            &amp;lt;/Link&amp;gt;
            {post.excerpt &amp;amp;&amp;amp; (
              &amp;lt;p className=&quot;mt-2 text-gray-600&quot;&amp;gt;{post.excerpt}&amp;lt;/p&amp;gt;
            )}
            {post.publishedAt &amp;amp;&amp;amp; (
              &amp;lt;time className=&quot;text-sm text-gray-400&quot;&amp;gt;
                {new Date(post.publishedAt).toLocaleDateString(&quot;en-US&quot;, {
                  year: &quot;numeric&quot;,
                  month: &quot;long&quot;,
                  day: &quot;numeric&quot;,
                })}
              &amp;lt;/time&amp;gt;
            )}
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice we're using &lt;code&gt;sanityFetch&lt;/code&gt; from &lt;code&gt;@/sanity/lib/live&lt;/code&gt; instead of calling the Sanity client directly. That's intentional — &lt;code&gt;sanityFetch&lt;/code&gt; is a wrapper that ties into Next.js's caching and revalidation system, and it's also what enables the live-update behavior in the next step.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-8-enable-sanity-live-in-the-root-layout&quot;&gt;Step 8: Enable Sanity Live in the Root Layout&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SanityLive&lt;/code&gt; is a component that subscribes to Sanity's Live Content API, so changes you publish in Studio show up on your site without a manual rebuild or redeploy.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/layout.tsx
import { SanityLive } from &quot;@/sanity/lib/live&quot;;

export default function RootLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;html
      lang=&quot;en&quot;
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    &amp;gt;
      &amp;lt;body className=&quot;min-h-full flex flex-col&quot;&amp;gt;
        {children}
        &amp;lt;SanityLive /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SanityLive&lt;/code&gt; doesn't render any visible UI — it just sits in the background and triggers revalidation whenever content changes in the Content Lake.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Placing &lt;code&gt;SanityLive&lt;/code&gt; only on specific pages instead of the root layout. Since it needs to listen for changes globally, the root layout is the correct place for it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-9-style-rich-text-with-tailwind-typography&quot;&gt;Step 9: Style Rich Text with Tailwind Typography&lt;/h3&gt;
&lt;p&gt;Portable Text renders as plain HTML elements with no default styling, so headings, paragraphs, and lists will look unstyled out of the box. The Tailwind Typography plugin fixes this in one line.&lt;/p&gt;
&lt;p&gt;Install it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @tailwindcss/typography
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the plugin to your global CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* app/globals.css */
@import &quot;tailwindcss&quot;;

@plugin &quot;@tailwindcss/typography&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll use the &lt;code&gt;prose&lt;/code&gt; class on any container wrapping Portable Text content in the next step.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-10-build-the-dynamic-post-page&quot;&gt;Step 10: Build the Dynamic Post Page&lt;/h3&gt;
&lt;p&gt;This is where everything comes together — fetching a single post by slug, pre-rendering it at build time, and rendering its rich text body with &lt;code&gt;PortableText&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx
import { sanityFetch } from &quot;@/sanity/lib/live&quot;;
import { allPostSlugsQuery, postBySlugQuery } from &quot;@/sanity/queries&quot;;
import { Post } from &quot;@/sanity/types&quot;;
import { PortableText } from &quot;next-sanity&quot;;
import Image from &quot;next/image&quot;;
import { notFound } from &quot;next/navigation&quot;;

// Tell Next.js which slugs to pre-render at build time
export async function generateStaticParams() {
  const { data: slugs } = await sanityFetch({ query: allPostSlugsQuery });
  return (slugs as { slug: string }[]).map(({ slug }) =&amp;gt; ({ slug }));
}

interface PageProps {
  params: Promise&amp;lt;{ slug: string }&amp;gt;;
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;
  const { data } = await sanityFetch({ query: postBySlugQuery, params: { slug } });

  if (!data) notFound(); // renders your app/not-found.tsx

  const post = data as Post;

  return (
    &amp;lt;article className=&quot;max-w-3xl mx-auto px-4 py-12&quot;&amp;gt;
      &amp;lt;h1 className=&quot;text-4xl font-bold mb-4&quot;&amp;gt;{post.title}&amp;lt;/h1&amp;gt;

      {post.publishedAt &amp;amp;&amp;amp; (
        &amp;lt;time className=&quot;text-sm text-gray-400 block mb-8&quot;&amp;gt;
          {new Date(post.publishedAt).toLocaleDateString(&quot;en-US&quot;, {
            year: &quot;numeric&quot;,
            month: &quot;long&quot;,
            day: &quot;numeric&quot;,
          })}
        &amp;lt;/time&amp;gt;
      )}

      {post.coverImage?.asset?.url &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;relative mb-8 rounded-xl overflow-hidden&quot;&amp;gt;
          &amp;lt;Image
            src={post.coverImage.asset.url}
            alt={post.title}
            className=&quot;rounded-xl&quot;
            priority
            width={1200}
            height={630}
          /&amp;gt;
        &amp;lt;/div&amp;gt;
      )}

      {Array.isArray(post.body) &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;prose prose-lg max-w-none&quot;&amp;gt;
          &amp;lt;PortableText value={post.body} /&amp;gt;
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A couple of things worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generateStaticParams&lt;/code&gt; runs at build time and tells Next.js exactly which &lt;code&gt;/blog/[slug]&lt;/code&gt; pages to statically generate, rather than rendering every post on demand&lt;/li&gt;
&lt;li&gt;&lt;code&gt;params&lt;/code&gt; is passed as a &lt;code&gt;Promise&lt;/code&gt; — this matches Next.js 16's async params convention, which is why &lt;code&gt;postBySlugQuery&lt;/code&gt; accepts it directly inside &lt;code&gt;sanityFetch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notFound()&lt;/code&gt; triggers your custom &lt;code&gt;not-found.tsx&lt;/code&gt; page if no post matches the slug, instead of crashing with an unhandled error&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting &lt;code&gt;Array.isArray(post.body)&lt;/code&gt; before rendering &lt;code&gt;PortableText&lt;/code&gt;. If &lt;code&gt;body&lt;/code&gt; is ever &lt;code&gt;undefined&lt;/code&gt; (e.g. an editor saved a post without adding body content), &lt;code&gt;PortableText&lt;/code&gt; will throw instead of failing gracefully.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;why-groq-instead-of-a-rest-or-graphql-api-&quot;&gt;Why GROQ instead of a REST or GraphQL API?&lt;/h4&gt;
&lt;p&gt;GROQ is built specifically for querying deeply nested, structured content in a single request — including resolving references like images — without the boilerplate of a separate GraphQL schema or multiple REST calls.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-to-deploy-sanity-studio-separately-&quot;&gt;Do I need to deploy Sanity Studio separately?&lt;/h4&gt;
&lt;p&gt;No. Since Studio is embedded inside your Next.js app at &lt;code&gt;/studio&lt;/code&gt;, it deploys automatically whenever your Next.js app deploys — no separate hosting or build step required.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-render-images-from-sanity-with-next-js-image-optimization-&quot;&gt;How do I render images from Sanity with Next.js Image optimization?&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;@sanity/image-url&lt;/code&gt; to build the image URL, then pass it to Next.js &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;. Add &lt;code&gt;cdn.sanity.io&lt;/code&gt; to the &lt;code&gt;images.remotePatterns&lt;/code&gt; array in &lt;code&gt;next.config.ts&lt;/code&gt; so Next.js is allowed to optimize it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
images: {
  remotePatterns: [{ protocol: &quot;https&quot;, hostname: &quot;cdn.sanity.io&quot; }],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;is-sanitylive-safe-to-use-in-production-&quot;&gt;Is SanityLive safe to use in production?&lt;/h4&gt;
&lt;p&gt;Yes. It's designed for production use and is the recommended way to get real-time content updates without manually managing webhooks or polling.&lt;/p&gt;
&lt;h4 id=&quot;is-the-sanity-studio-accessible-to-the-public-&quot;&gt;Is the Sanity Studio accessible to the public?&lt;/h4&gt;
&lt;p&gt;By default, yes — anyone who visits &lt;code&gt;/studio&lt;/code&gt; can see it (though they can't edit without a Sanity account with the right permissions). To fully restrict it, wrap the studio route in a middleware auth check or only run it in dev using an environment variable guard.&lt;/p&gt;
&lt;h4 id=&quot;how-is-this-different-from-my-current-mdx-based-blog-&quot;&gt;How is this different from my current MDX-based blog?&lt;/h4&gt;
&lt;p&gt;With MDX, content lives as files in your repo — every edit means a commit and redeploy. With Sanity, content lives in the cloud and can be edited through a UI, published instantly, and even handed off to non-technical writers without giving them repo access.&lt;/p&gt;
&lt;h4 id=&quot;can-i-add-more-content-types-later-like-authors-or-categories-&quot;&gt;Can I add more content types later, like authors or categories?&lt;/h4&gt;
&lt;p&gt;Yes. Create a new schema file the same way you created &lt;code&gt;post.ts&lt;/code&gt;, then register it in the &lt;code&gt;types&lt;/code&gt; array inside &lt;code&gt;schemaTypes/index.ts&lt;/code&gt;. Studio will pick it up immediately.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;You now have a fully working Sanity CMS integration inside a Next.js 16 application — complete with an embedded Studio, a typed content model, GROQ-powered queries, and live content updates. From here, you can expand your schema with authors, categories, or custom block content, add Visual Editing for inline previews, or wire up on-demand revalidation through Sanity's webhooks.&lt;/p&gt;
&lt;p&gt;This setup scales well beyond a simple blog — the same pattern works for landing pages, product catalogs, or any structured content your app needs to manage outside the codebase.&lt;/p&gt;
&lt;h3 id=&quot;useful-resources&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sanity.io/learn/course/content-driven-web-application-foundations/create-a-new-sanity-project&quot;&gt;Sanity: Create a New Sanity Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sanity.io/learn/course/content-driven-web-application-foundations/the-next-sanity-toolkit&quot;&gt;Sanity: The next-sanity Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sanity.io/docs/groq&quot;&gt;Sanity GROQ Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.portabletext.org/&quot;&gt;Portable Text Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;p&gt;If you'd like to keep building on this foundation, check out these related guides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/saas-starter-architecture-nextjs&quot;&gt;SaaS Starter Architecture with Next.js 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📦 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Muhammad-Ali-sma/Sanity-Integration-in-Next.js-16&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23495/how-to-integrate-sanity-cms-with-next-js-16-app-router</guid>
<pubDate>Sun, 26 Jul 2026 11:37:52 +0000</pubDate>
</item>
</channel>
</rss>