Sanity CMS with Nextjs
Introduction
This guide covers the integration of Sanity CMS into a [Link] project. Sanity CMS
is a headless content management system that provides a flexible and powerful
way to manage content. By integrating Sanity CMS, you can build an advanced
studio (dashboard) within your [Link] application, facilitating easier content
management.
What is a Headless CMS?
A headless CMS is a backend-only content management system that decouples
the content repository from the presentation layer. Unlike traditional CMSs, which
tightly couple content management with the frontend, a headless CMS allows
developers to deliver content across various devices and platforms using APIs.
Advantages of a Headless CMS
Flexibility: Content can be delivered to any frontend framework or device,
providing a versatile solution for modern web development.
Scalability: Headless CMSs are designed to handle high traffic and large
volumes of content, making them suitable for growing businesses.
Improved Performance: By separating the content management and
presentation layers, you can optimize each independently, leading to faster
load times and better user experiences.
How It Works
Sanity CMS with Nextjs 1
1. Schemas: In Sanity CMS, schemas define the structure of your content. You
create schemas to specify what fields your content types (e.g., blogs, authors)
will have.
2. API: Sanity provides a powerful API to query and manage your content. You
can use GROQ (Graph-Relational Object Queries) to fetch and manipulate
data.
3. Studio: The Sanity Studio is a customizable, React-based interface where
content editors can manage and organize content. It is integrated into the
[Link] application to provide a seamless content management experience.
By following this guide, you will learn how to set up a [Link] project, configure
Sanity CMS, and create schemas and queries to manage your content effectively.
Technologies Used
Nextjs 14 (App Router)
Tailwind CSS
TypeScript
Sanity
Project Setup
Create a new directory for the project.
Sign up to the Sanity website and get the SANITY_API_READ_TOKEN,
NEXT_PUBLIC_SANITY_DATASET, and NEXT_PUBLIC_SANITY_PROJECT_ID.
Paste the below command to initialize a nextjs project. (you may also use
npm/yarn/bun)
pnpx create-next-app@latest
pnpm create sanity@latest
Exchange the [Link] file to install all dependencies.
Sanity CMS with Nextjs 2
{
"name": "sanity-blog",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,cs
},
"dependencies": {
"@portabletext/react": "^3.1.0",
"@sanity/client": "^6.21.1",
"@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.53.0",
"@types/node": "22.1.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"autoprefixer": "10.4.20",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eslint": "9.8.0",
"eslint-config-next": "14.2.5",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.426.0",
"mongoose": "^8.5.2",
"next": "14.2.5",
"next-sanity": "^9.4.4",
"next-themes": "^0.3.0",
"postcss": "8.4.41",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.52.2",
Sanity CMS with Nextjs 3
"sanity": "^3.53.0",
"styled-components": "^6.1.12",
"tailwind-merge": "^2.4.0",
"tailwindcss": "3.4.9",
"typescript": "5.5.4",
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@tailwindcss/typography": "^0.5.14",
"@tanstack/eslint-plugin-query": "^5.51.15",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5"
}
}
App Structure
├── app
│ ├── [Link]
│ ├── [Link]
│ ├── blogs
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [slug]
│ │ │ └── [Link]
│ │ └── add
│ │ └── [[...index]]/[Link]
├── components
│ ├── ui
│ │ ├── [Link]
│ │ └── ...
Sanity CMS with Nextjs 4
│ ├── [Link]
│ └── ...
├── sanity
│ ├── lib
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── schemas
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ └── schemas
├── styles
│ └── [Link]
├── [Link]
├── [Link]
├── [Link]
└── [Link]
Environment Variables (.env)
SANITY_API_READ_TOKEN=
NEXT_PUBLIC_SANITY_DATASET=
NEXT_PUBLIC_SANITY_PROJECT_ID=
Sanity configuration
Sign up to [Link].
Sanity CMS with Nextjs 5
Create a new project.
Generate Token for your project.
Sanity CMS with Nextjs 6
Add [Link]
/**
* This configuration is used to for the Sanity Studio that’s
*/
import { visionTool } from "@sanity/vision"
import { defineConfig } from "sanity"
import { deskTool } from "sanity/desk"
// Go to [Link] to learn h
import { apiVersion, dataset, projectId } from "./sanity/env"
import { schema } from "./sanity/schema"
export default defineConfig({
basePath: "/blogs/add",
projectId,
dataset,
// Add and edit the content schema in the './sanity/schema
schema,
plugins: [
deskTool(),
// Vision is a tool that lets you query your content with
// [Link]
Sanity CMS with Nextjs 7
visionTool({ defaultApiVersion: apiVersion }),
],
})
Add [Link]
/**
* This configuration file lets you run `$ sanity [command]`
* Go to [Link] to learn more.
**/
import { defineCliConfig } from "sanity/cli"
const projectId = [Link].NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = [Link].NEXT_PUBLIC_SANITY_DATASET
export default defineCliConfig({ api: { projectId, dataset }
Sanity Integration
We will add schemas and queries to get our data to add sanity.
/lib folder
/[Link]
import { createClient } from "next-sanity"
import { apiVersion, dataset, projectId, useCdn } from ".
export const client = createClient({
apiVersion,
dataset,
projectId,
useCdn,
})
Sanity CMS with Nextjs 8
/[Link]
import { client } from "@/sanity/lib/client"
import createImageUrlBuilder from "@sanity/image-url"
import imageUrlBuilder from "@sanity/image-url"
import type { Image } from "sanity"
import { dataset, projectId } from "../env"
const builder = imageUrlBuilder(client)
const imageBuilder = createImageUrlBuilder({
projectId: projectId || "",
dataset: dataset || "",
})
export const urlForImage = (source: Image) => {
return imageBuilder?.image(source).auto("format").fit("m
}
export function getImgUrl(obj: object, width: number = 300
if (!obj) return "/[Link]"
return [Link](obj).width(width).height(height).ur
}
/[Link]
// ./nextjs-app/sanity/lib/[Link]
import { groq } from "next-sanity"
// Get all blogs
export const blogsQuery = groq`*[_type == "blog" && define
_id, title, slug, mainImage, publishedAt, description
}`
Sanity CMS with Nextjs 9
// Get a single blog by its slug
export const blogQuery = groq`*[_type == "blog" && [Link]
title, mainImage, body, author -> {name, image}, publi
}`
// Get a single blog by its slug
export const blogMetaDataQuery = groq`*[_type == "blog" &&
title, mainImage
}`
// Get all blog slugs
export const blogPathsQuery = groq`*[_type == "blog" && de
"params": { "slug": [Link] }
}`
[Link]
import "server-only"
import { draftMode } from "next/headers"
import { client } from "@/sanity/lib/client"
import type { QueryParams } from "@sanity/client"
const DEFAULT_PARAMS = {} as QueryParams
const DEFAULT_TAGS = [] as string[]
export const token = [Link].SANITY_API_READ_TOKEN
export async function sanityFetch<QueryResponse>({
query,
params = DEFAULT_PARAMS,
tags = DEFAULT_TAGS,
}: {
query: string
Sanity CMS with Nextjs 10
params?: QueryParams
tags?: string[]
}): Promise<QueryResponse> {
const isDraftMode = draftMode().isEnabled
if (isDraftMode && !token) {
throw new Error("The `SANITY_API_READ_TOKEN` environme
}
// const isDevelopment = [Link].NODE_ENV === "devel
return [Link]({ useCdn: false }).fetch<QueryR
// cache: isDevelopment || isDraftMode ? undefined : "
...(isDraftMode && {
token: token,
perspective: "previewDrafts",
}),
next: {
...(isDraftMode && { revalidate: 30 }),
tags,
},
})
}
/schemas folder
/[Link]
import { defineField, defineType } from "sanity"
export default defineType({
name: "author",
title: "Author",
type: "document",
fields: [
defineField({
name: "name",
Sanity CMS with Nextjs 11
title: "Name",
type: "string",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "name",
maxLength: 96,
},
}),
defineField({
name: "image",
title: "Image",
type: "image",
options: {
hotspot: true,
},
fields: [
{
name: "alt",
type: "string",
title: "Alternative Text",
},
],
}),
defineField({
name: "bio",
title: "Bio",
type: "array",
of: [
{
title: "Block",
type: "block",
styles: [{ title: "Normal", value: "normal" }],
Sanity CMS with Nextjs 12
lists: [],
},
],
}),
],
preview: {
select: {
title: "name",
media: "image",
},
},
})
/[Link]
import { defineArrayMember, defineType } from "sanity"
export default defineType({
title: "Block Content",
name: "blockContent",
type: "array",
of: [
defineArrayMember({
title: "Block",
type: "block",
styles: [
{ title: "Normal", value: "normal" },
{ title: "H1", value: "h1" },
{ title: "H2", value: "h2" },
{ title: "H3", value: "h3" },
{ title: "H4", value: "h4" },
{ title: "Quote", value: "blockquote" },
],
lists: [{ title: "Bullet", value: "bullet" }],
marks: {
Sanity CMS with Nextjs 13
decorators: [
{ title: "Strong", value: "strong" },
{ title: "Emphasis", value: "em" },
],
annotations: [
{
title: "URL",
name: "link",
type: "object",
fields: [
{
title: "URL",
name: "href",
type: "url",
},
],
},
],
},
}),
defineArrayMember({
type: "image",
options: { hotspot: true },
fields: [
{
name: "alt",
type: "string",
title: "Alternative Text",
},
],
}),
],
})
/[Link]
Sanity CMS with Nextjs 14
import { defineField, defineType } from "sanity"
export default defineType({
name: "blog",
title: "Blog",
type: "document",
fields: [
defineField({
name: "title",
title: "Title",
type: "string",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "title",
maxLength: 96,
},
}),
defineField({
name: "author",
title: "Author",
type: "reference",
to: { type: "author" },
}),
defineField({
name: "mainImage",
title: "Main image",
type: "image",
options: {
hotspot: true,
},
fields: [
Sanity CMS with Nextjs 15
{
name: "alt",
type: "string",
title: "Alternative Text",
},
],
}),
defineField({
name: "tags",
title: "Tags",
type: "array",
of: [{ type: "reference", to: { type: "tags" } }],
}),
defineField({
name: "publishedAt",
title: "Published at",
type: "datetime",
}),
defineField({
name: "body",
title: "Body",
type: "blockContent",
}),
],
preview: {
select: {
title: "title",
author: "[Link]",
media: "mainImage",
},
prepare(selection) {
const { author } = selection
return { ...selection, subtitle: author && `by ${aut
},
Sanity CMS with Nextjs 16
},
})
/[Link]
import { defineField, defineType } from "sanity"
export default defineType({
name: "tags",
title: "Tags",
type: "document",
fields: [
defineField({
name: "tag",
title: "Tag",
type: "string",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "tag",
maxLength: 24,
},
}),
defineField({
name: "active",
title: "Active",
type: "boolean",
}),
],
})
[Link]
Sanity CMS with Nextjs 17
export const apiVersion = [Link].NEXT_PUBLIC_SANITY_API_
export const dataset = assertValue([Link].NEXT_PUBLIC_SA
export const useCdn = false
export const projectId = assertValue(
[Link].NEXT_PUBLIC_SANITY_PROJECT_ID,
"Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_I
)
function assertValue<T>(v: T | undefined, errorMessage: strin
if (v === undefined) {
throw new Error(errorMessage)
}
return v
}
[Link]
import { type SchemaTypeDefinition } from "sanity"
import author from "./schemas/author"
import blockContent from "./schemas/block-content"
import blog from "./schemas/blog"
import tags from "./schemas/tags"
export const schema: { types: SchemaTypeDefinition[] } = {
types: [blog, author, tags, blockContent],
}
Frontend(UI) Setup
Here, It has mainly 4 pages (routes).
Sanity CMS with Nextjs 18
├── /
│ ├── /blogs
│ │ ├── /add
│ │ └── /[slug]
Home route
homepage
import Navbar from "@/components/navbar"
export default function HomeLayout({ children }: { childre
return (
<>
<Navbar />
<p> Make a landing page for your blog. </p>
</>
)
}
Blog route
[Link]
import type { Metadata } from "next"
import { getImgUrl } from "@/sanity/lib/image"
import { blogsQuery } from "@/sanity/lib/queries"
import { sanityFetch } from "@/sanity/lib/sanity-fetch"
import { formatDMY } from "@/utils/helper"
import { SanityDocument } from "next-sanity"
import BlogCard from "./blog-card"
export default async function BlogPag() {
const blogs = await sanityFetch<SanityDocument[]>({ quer
Sanity CMS with Nextjs 19
return (
<section>
<div className="-mx-4 flex flex-wrap text-foreground
<div className="w-full px-4">
<div className="mx-auto mb-[60px] max-w-[550px]
<h1 className="mb-2 block text-3xl font-semibo
<p className="text-body-color text-base">
Here, you'll find articles on a variety
as well as general topics of interest to our
</p>
</div>
</div>
</div>
<div className="-mx-4 flex flex-wrap">
{blogs && [Link] > 0 ? (
[Link]((blog) => {
return (
<BlogCard
key={blog._id}
slug={[Link]}
date={formatDMY([Link])}
cardTitle={[Link]}
cardDescription={[Link]}
image={getImgUrl([Link], 300, 250)
/>
)
})
) : (
<p>Blogs Not Available!</p>
)}
</div>
</section>
)
Sanity CMS with Nextjs 20
}
const title = "Interio | Blogs"
const description = `Here, you'll find articles on a varie
export const metadata: Metadata = {
title,
description,
// openGraph: {
// title,
// description,
// type: "website",
// url: "[Link]
// siteName: "example",
// images: [
// {
// url: "[Link]
// height: 480,
// width: 480,
// },
// ],
// },
}
add/[[…index]/[Link]
"use client"
/**
* This route is responsible for the built-in authoring en
* All routes under your studio path is handled by this fi
* [Link]
*
* You can learn more about the next-sanity package here:
* [Link]
Sanity CMS with Nextjs 21
*/
import config from "@/[Link]"
import { NextStudio } from "next-sanity/studio"
export default function StudioPage() {
return <NextStudio config={config} />
}
[slug]/[Link]
import Image from "next/image"
import { client } from "@/sanity/lib/client"
import { getImgUrl } from "@/sanity/lib/image"
import {
blogPathsQuery,
blogQuery,
} from "@/sanity/lib/queries"
import { sanityFetch } from "@/sanity/lib/sanity-fetch"
import { formatDMY } from "@/utils/helper"
import { PortableText } from "@portabletext/react"
import { SanityDocument } from "@sanity/client"
import { BlogComponents } from "@/components/custom-ptc"
type PageProps = {
params: {
slug: string
}
}
export async function generateStaticParams() {
const blogs = await [Link](blogPathsQuery)
return blogs
}
Sanity CMS with Nextjs 22
export default async function Page({ params }: PageProps)
const blog = await sanityFetch<SanityDocument>({ query:
// [Link](blog)
if (!blog) {
return (
<main className="container">
<Image src="/[Link]" alt="Not Found Image"
<h1 className="relative -top-10 text-center text-r
</main>
)
}
return (
<main className="prose prose-lg mx-auto text-foregroun
<h1 className="text-foreground">{[Link]}</h1>
{blog?.mainImage && (
<Image
className="my-4 rounded-lg"
src={getImgUrl([Link], 800, 400)}
width={800}
height={400}
alt={blog?.mainImage?.alt || "Blog Image"}
/>
)}
{blog?.body ? <PortableText value={[Link]} compon
{[Link] && (
<section>
<h3 className="m-0">Author</h3>
<div className="flex h-fit items-center">
<Image
src={getImgUrl([Link]?.image, 40, 40)}
width={40}
height={40}
alt={[Link]?.image?.alt}
className="m-0 mr-2 rounded-full"
Sanity CMS with Nextjs 23
/>
<p className="capitalize">{blog?.author?.name}
</div>
<p className="m-0">Published On: {formatDMY(blog
</section>
)}
</main>
)
}
export function formatDMY(date: string, weekday?: boolean)
const options: {
year: string
month: string
day: string
weekday?: string
} = { year: "numeric", month: "long", day: "numeric" }
if (weekday) {
[Link] = "short"
}
// @ts-ignore -d s
return new Date(date).toLocaleDateString(undefined, opti
}
Useful Resources
1. [Link]
2. [Link]
3. [Link]
Sanity CMS with Nextjs 24