惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LangChain Blog
G
Google Developers Blog
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
DataBreaches.Net
C
Check Point Blog
月光博客
月光博客
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs
MyScale Blog
MyScale Blog
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
博客园_首页
S
SegmentFault 最新的问题
The Cloudflare Blog
NISL@THU
NISL@THU
T
The Exploit Database - CXSecurity.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
U
Unit 42
S
Schneier on Security
博客园 - 司徒正美
Help Net Security
Help Net Security
O
OpenAI News
PCI Perspectives
PCI Perspectives
T
Troy Hunt's Blog
Microsoft Azure Blog
Microsoft Azure Blog
Forbes - Security
Forbes - Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
V
Vulnerabilities – Threatpost
美团技术团队
P
Palo Alto Networks Blog
TaoSecurity Blog
TaoSecurity Blog
V2EX - 技术
V2EX - 技术
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How to Integrate Sanity CMS with Next.js 16 App Router (2026 Guide)
Syed Muhammad Ali · 2026-06-26 · via DEV Community

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.

That's exactly where Sanity CMS 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.

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.


Why Sanity Over Other Headless CMS Options?

There are plenty of headless CMS choices out there — Contentful, Strapi, Payload — so why Sanity?

  • Sanity Studio runs inside your Next.js app at /studio, so there's no separate admin URL to manage.
  • GROQ (Graph-Relational Object Queries) is more flexible and readable than GraphQL for content queries.
  • Real-time collaboration — multiple editors can work on content simultaneously.
  • Free tier is genuinely generous for most personal projects and small clients.

What We're Building

  • An embedded Sanity Studio running at /studio inside your Next.js app
  • A post schema with title, slug, excerpt, cover image, and rich text body
  • GROQ queries to fetch all posts and a single post by slug
  • Fully typed Sanity responses with TypeScript
  • A blog listing page and a dynamic post page using generateStaticParams
  • Real-time content updates using Sanity Live

What Is Sanity, Exactly?

Sanity is a headless CMS — 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).

💡 Tip: Think of Sanity Studio as a "window" 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.


Prerequisites

Before we start, make sure you have:

  • Node.js 20+ installed
  • A Next.js 16 project with the App Router (TypeScript + Tailwind)
  • A free account at sanity.io

- Basic familiarity with Next.js file-based routing

Step 1: Create a Sanity Project

First, let's create a new Sanity project from the CLI. If you don't have the Sanity CLI installed yet, run:

npm install -g @sanity/cli

Then inside your Next.js project root, initialize Sanity:

npx sanity@latest init

The CLI will walk you through a series of prompts. Here's exactly what to choose:

✔ 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

⚠️ Common Mistake: Picking the "blog" template instead of "Clean project" gives you pre-built schema types (post, author, category). That's fine if you want a head start, but this guide builds the post schema manually so you understand exactly what each field does — so we go with the clean template.

Once it finishes, your project folder should look like this:

.
├── .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

What Just Got Created?

A few of these files matter more than others:

  • src/sanity/lib/client.ts — a configured Sanity Client instance used to query content
  • src/sanity/lib/live.ts — exports sanityFetch and SanityLive, which power real-time content updates
  • src/sanity/env.ts — reads your project ID and dataset from .env.local
  • sanity.config.ts — configures Studio itself: schema types, plugins, and the studio route
  • app/studio/[[...tool]]/page.tsx — the catch-all route that renders the entire Studio UI at /studio

Your .env.local should now contain:

NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

These are prefixed with NEXT_PUBLIC_ 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.


Step 2: Install next-sanity

The CLI usually installs this for you, but if it's missing, add it manually:

npm install next-sanity

next-sanity 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.


Step 3: Create a Post Schema

Schemas in Sanity define what content editors are allowed to create in Studio — think of them as the shape of your content, similar to a database model.

Create a new schema file:

// src/sanity/schemaTypes/post.ts
import { defineField, defineType } from "sanity";

export const postSchema = defineType({
  name: "post",
  title: "Blog Post",
  type: "document",
  fields: [
    defineField({
      name: "title",
      title: "Title",
      type: "string",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "slug",
      title: "Slug",
      type: "slug",
      options: { source: "title" }, // auto-generates slug from title
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "excerpt",
      title: "Excerpt",
      type: "text",
      rows: 3,
    }),
    defineField({
      name: "coverImage",
      title: "Cover Image",
      type: "image",
      options: { hotspot: true }, // lets editors set a focal point
    }),
    defineField({
      name: "publishedAt",
      title: "Published At",
      type: "datetime",
    }),
    defineField({
      name: "body",
      title: "Body",
      type: "array",
      of: [{ type: "block" }, { type: "image" }], // rich text + inline images
    }),
  ],
  orderings: [
    {
      title: "Published Date, New",
      name: "publishedAtDesc",
      by: [{ field: "publishedAt", direction: "desc" }],
    },
  ],
});

A quick breakdown of what's happening:

  • defineType registers a new document type — editors will see "Blog Post" as a creatable content type in Studio
  • defineField describes each individual field and its validation rules
  • The slug field with options: { source: "title" } auto-generates a URL-friendly slug from the title as the editor types
  • body uses type: "array" with block and image — this is Sanity's Portable Text format, a structured way of representing rich text that isn't tied to HTML

💡 Tip: Rule.required() enforces validation inside Studio itself. Editors literally cannot publish a post without a title or slug — no backend validation needed.


Step 4: Register the Schema

Defining a schema isn't enough — Studio needs to know it exists. Register it in the schema index:

// src/sanity/schemaTypes/index.ts
import { type SchemaTypeDefinition } from "sanity";
import { postSchema } from "./post";

export const schema: { types: SchemaTypeDefinition[] } = {
  types: [postSchema],
};

⚠️ Common Mistake: Forgetting this step is the #1 reason a new schema "doesn't show up" in Studio. Defining the type isn't enough — it has to be added to this types array.

Now start your dev server and visit http://localhost:3000/studio. You should see "Blog Post" listed as a content type, ready to create your first post.


Step 5: Write GROQ Queries

GROQ (Graph-Relational Object Queries) is Sanity's query language, purpose-built for querying structured, nested JSON-like content.

💡 Tip: 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.

Create a queries file:

// src/sanity/queries.ts
import { groq } from "next-sanity";

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

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

// Fetch all slugs for generateStaticParams
export const allPostSlugsQuery = groq`
  *[_type == "post"] { "slug": slug.current }
`;

Here's what each part of the first query means:

  • *[_type == "post"] — selects every document where the type equals "post"
  • | order(publishedAt desc) — sorts results by publish date, newest first
  • { ... } — a projection, meaning "only return these fields," similar to selecting specific columns in SQL
  • coverImage { asset-> { _id, url } } — the -> is a dereference, following the image asset reference to pull its actual URL

The second query uses $slug as a parameter and [0] to grab just the first matching result, since slugs are unique.


Step 6: Add TypeScript Types

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:

// 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
}

💡 Tip: Sanity also offers a code generation tool (sanity typegen) 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.


Step 7: Build the Blog Listing Page

With queries and types ready, fetching content becomes straightforward:

// app/blog/page.tsx
import { sanityFetch } from "@/sanity/lib/live";
import { allPostsQuery } from "@/sanity/queries";
import { Post } from "@/sanity/types";
import Link from "next/link";

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

  return (
    <main className="max-w-3xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>
      <ul className="space-y-8">
        {(posts as Post[]).map((post) => (
          <li key={post._id} className="border-b pb-8">
            <Link href={`/blog/${post.slug.current}`}>
              <h2 className="text-2xl font-semibold hover:underline">
                {post.title}
              </h2>
            </Link>
            {post.excerpt && (
              <p className="mt-2 text-gray-600">{post.excerpt}</p>
            )}
            {post.publishedAt && (
              <time className="text-sm text-gray-400">
                {new Date(post.publishedAt).toLocaleDateString("en-US", {
                  year: "numeric",
                  month: "long",
                  day: "numeric",
                })}
              </time>
            )}
          </li>
        ))}
      </ul>
    </main>
  );
}

Notice we're using sanityFetch from @/sanity/lib/live instead of calling the Sanity client directly. That's intentional — sanityFetch 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.


Step 8: Enable Sanity Live in the Root Layout

SanityLive 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.

// app/layout.tsx
import { SanityLive } from "@/sanity/lib/live";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">
        {children}
        <SanityLive />
      </body>
    </html>
  );
}

SanityLive doesn't render any visible UI — it just sits in the background and triggers revalidation whenever content changes in the Content Lake.

⚠️ Common Mistake: Placing SanityLive 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.


Step 9: Style Rich Text with Tailwind Typography

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.

Install it:

npm install @tailwindcss/typography

Add the plugin to your global CSS:

/* app/globals.css */
@import "tailwindcss";

@plugin "@tailwindcss/typography";

You'll use the prose class on any container wrapping Portable Text content in the next step.


Step 10: Build the Dynamic Post Page

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 PortableText.

// app/blog/[slug]/page.tsx
import { sanityFetch } from "@/sanity/lib/live";
import { allPostSlugsQuery, postBySlugQuery } from "@/sanity/queries";
import { Post } from "@/sanity/types";
import { PortableText } from "next-sanity";
import Image from "next/image";
import { notFound } from "next/navigation";

// 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 }) => ({ slug }));
}

interface PageProps {
  params: Promise<{ slug: string }>;
}

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 (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>

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

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

      {Array.isArray(post.body) && (
        <div className="prose prose-lg max-w-none">
          <PortableText value={post.body} />
        </div>
      )}
    </article>
  );
}

A couple of things worth calling out:

  • generateStaticParams runs at build time and tells Next.js exactly which /blog/[slug] pages to statically generate, rather than rendering every post on demand
  • params is passed as a Promise — this matches Next.js 16's async params convention, which is why postBySlugQuery accepts it directly inside sanityFetch
  • notFound() triggers your custom not-found.tsx page if no post matches the slug, instead of crashing with an unhandled error

⚠️ Common Mistake: Forgetting Array.isArray(post.body) before rendering PortableText. If body is ever undefined (e.g. an editor saved a post without adding body content), PortableText will throw instead of failing gracefully.


Frequently Asked Questions

Why GROQ instead of a REST or GraphQL API?

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.

Do I need to deploy Sanity Studio separately?

No. Since Studio is embedded inside your Next.js app at /studio, it deploys automatically whenever your Next.js app deploys — no separate hosting or build step required.

How do I render images from Sanity with Next.js Image optimization?

Use @sanity/image-url to build the image URL, then pass it to Next.js <Image>. Add cdn.sanity.io to the images.remotePatterns array in next.config.ts so Next.js is allowed to optimize it:

// next.config.ts
images: {
  remotePatterns: [{ protocol: "https", hostname: "cdn.sanity.io" }],
}

Is SanityLive safe to use in production?

Yes. It's designed for production use and is the recommended way to get real-time content updates without manually managing webhooks or polling.

Is the Sanity Studio accessible to the public?

By default, yes — anyone who visits /studio 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.

How is this different from my current MDX-based blog?

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.

Can I add more content types later, like authors or categories?

Yes. Create a new schema file the same way you created post.ts, then register it in the types array inside schemaTypes/index.ts. Studio will pick it up immediately.


Conclusion

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.

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.

Useful Resources

Continue Learning

If you'd like to keep building on this foundation, check out these related guides:

📦 Source Code: View on GitHub