35% OFF
Ends14d 00:00:00
Shop
Back to Blog
nextjssanitytypescriptwebdev

How to Use Sanity CMS with Next.js 15 (Setup, Queries, and Live Preview)

Pixel Anas··6 min read

A complete guide to integrating Sanity CMS with Next.js 15, from initial setup to querying content and enabling live preview for editors.

If you are building a blog, marketing site, or anything with content that needs to be edited without touching code, at some point MongoDB or a hand-rolled admin panel starts to feel like the wrong tool. This is usually where Sanity CMS comes in.

Sanity handles the content editing side, a real interface non-technical people can use, while Next.js handles rendering it. Here is how the two actually fit together.

Why Sanity Instead of a Database You Already Have

If you already run MongoDB for your app's core data, users, orders, application state, it is tempting to just store blog content there too. The problem shows up the moment someone other than a developer needs to publish a post. A raw database has no editing interface, no image handling, no draft and publish workflow.

Sanity gives you a real content studio, a proper editing UI, without you building one from scratch. It stores content as structured data, not raw HTML, which means the same content can render differently depending on where it is displayed. For a project where content and app data are genuinely separate concerns, keeping them in separate systems, MongoDB for app data and Sanity for editorial content, tends to be cleaner than forcing both into one database.

Setting Up Sanity in a Next.js 15 Project

npm create sanity@latest

This scaffolds a Sanity Studio, either as a standalone project or embedded inside your existing Next.js app under a route like /studio. For most projects, embedding it directly means one deployment instead of two separate ones to manage.

// sanity.config.ts
import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure';
import { schemaTypes } from './schemas';

export default defineConfig({
  name: 'default',
  title: 'My Blog',
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID as string,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET as string,
  plugins: [structureTool()],
  schema: {
    types: schemaTypes,
  },
});
// app/studio/[[...tool]]/page.tsx
import { NextStudio } from 'next-sanity/studio';
import config from '../../../sanity.config';

export default function StudioPage() {
  return <NextStudio config={config} />;
}

Visiting /studio now shows the actual Sanity editing interface, running inside your Next.js app.

Defining a Content Schema

Schemas define what fields a content type has, which shapes both the editing form in Studio and the data structure you query later.

// schemas/post.ts
import { defineField, defineType } from 'sanity';

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'slug',
      type: 'slug',
      options: { source: 'title' },
    }),
    defineField({
      name: 'coverImage',
      type: 'image',
      options: { hotspot: true },
    }),
    defineField({
      name: 'body',
      type: 'array',
      of: [{ type: 'block' }],
    }),
    defineField({
      name: 'publishedAt',
      type: 'datetime',
    }),
  ],
});

slug with source: 'title' auto-generates a URL-friendly slug from the title as an editor types, and body as an array of block types is what enables Sanity's rich text editor rather than a plain textarea.

Querying Content with GROQ

Sanity uses its own query language, GROQ, instead of REST or GraphQL by default.

// lib/sanity/queries.ts
import { client } from './client';

export async function getPosts() {
  return client.fetch(`
    *[_type == "post"] | order(publishedAt desc) {
      title,
      "slug": slug.current,
      coverImage,
      publishedAt
    }
  `);
}

export async function getPost(slug: string) {
  return client.fetch(
    `*[_type == "post" && slug.current == $slug][0] {
      title,
      body,
      coverImage,
      publishedAt
    }`,
    { slug }
  );
}
// lib/sanity/client.ts
import { createClient } from 'next-sanity';

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID as string,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET as string,
  apiVersion: '2024-01-01',
  useCdn: true,
});

useCdn: true serves cached responses for faster reads, appropriate for published content that does not need to reflect edits within milliseconds.

Rendering Sanity Content in a Server Component

// app/blog/[slug]/page.tsx
import { getPost } from '@/lib/sanity/queries';
import { PortableText } from '@portabletext/react';
import { notFound } from 'next/navigation';

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <PortableText value={post.body} />
    </article>
  );
}

PortableText renders Sanity's rich text block format as actual HTML, handling headings, bold text, links, and embedded images correctly without you writing a custom renderer.

Enabling Live Preview for Editors

Editors generally do not want to publish a post just to see how it looks. Sanity's draft mode integration with Next.js solves this.

// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const slug = searchParams.get('slug');

  const draft = await draftMode();
  draft.enable();

  redirect(`/blog/${slug}`);
}
// lib/sanity/queries.ts
export async function getPost(slug: string, preview = false) {
  return client.fetch(
    `*[_type == "post" && slug.current == $slug][0] { ... }`,
    { slug },
    { perspective: preview ? 'previewDrafts' : 'published' }
  );
}

With draft mode enabled, the query fetches unpublished draft content instead of only published posts, letting an editor click a preview link in Studio and see exactly how an unpublished change will look before it goes live.

Frequently Asked Questions

Can I use Sanity CMS and MongoDB in the same Next.js project? Yes, and it is a common setup. Sanity handles editorial content like blog posts and marketing copy, while MongoDB handles application data like users, orders, or anything tied to app logic. Keeping them separate avoids forcing editorial workflows into a database that was never built for content editing.

Is Sanity free to use? Sanity has a free tier suitable for most small to mid-sized projects, with usage-based pricing once you exceed certain limits on API requests, bandwidth, or team seats.

Does Sanity work with Next.js Server Components? Yes. Sanity's client fetches over HTTP, which works cleanly inside async Server Components the same way any other data fetching does, including compatibility with Next.js caching behavior since the underlying requests go through fetch.

What is GROQ and do I need to learn a new query language? GROQ is Sanity's own query language, and yes, it is different from SQL, GraphQL, or MongoDB's query syntax. It is generally considered easier to pick up than it looks at first, especially for straightforward content queries like fetching a list of posts or a single document by slug.


If you are setting up a content-driven site and want to see this pattern in a real, working project rather than just a description, I build this exact CMS-plus-Next.js setup into client projects and templates.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751