How to Connect Sanity CMS with Next.js 14 (Step-by-Step Guide)
Are you a web developer building with Next.js 14 and looking to manage dynamic content without compromising frontend flexibility? The challenge often lies in finding a content management system that integrates seamlessly and empowers developers with full control over data presentation. This is precisely where a headless CMS like Sanity.io, paired with Next.js, becomes invaluable. This comprehensive guide will walk you through how to connect Sanity to Next.js 14, enabling you to fetch and display dynamic content for your projects.
Introduction: Why Use a Headless CMS with Next.js?
In modern web development, separating your content from your presentation layer offers immense benefits. A headless CMS solely provides the backend for content creation and management. The frontend, responsible for displaying this content, is then built separately using frameworks like Next.js or React. This separation allows for greater flexibility and scalability, as you're not tied to a monolithic system.
Using a headless CMS with Next.js means:
- Separation of Concerns: Content creators can manage content without needing developer intervention, and developers can focus on building a robust user interface.
- Performance: Next.js's capabilities like Server Components and static site generation can pre-render content, leading to incredibly fast load times.
- Developer Freedom: You have complete control over how your content is structured and displayed, leveraging Next.js 14's features for optimal performance and user experience.
Mastering these modern web development techniques is a core part of what you'll learn in Juno's Web Development Full Course in Hindi.
Step 1: Setting Up Your Sanity.io Project
The first step in this Sanity Next.js tutorial is to establish your Sanity.io project. This will serve as your content repository.
1.1 Create a New Project in Sanity.io Dashboard
Navigate to manage.sanity.io and log in or sign up. Click "Create new project" and give it a descriptive name. Choose a starter template (e.g., "Blog") or start with an empty project. This dashboard will manage your project settings, API keys, and datasets.
1.2 Install the Sanity CLI
Open your terminal or command prompt and install the Sanity Command Line Interface globally:
npm install -g @sanity/cli
1.3 Initialize a New Sanity Studio Locally
In your terminal, navigate to the directory where you want to create your Sanity Studio project. Then, run:
sanity init
Follow the prompts:
- Select "Create new project" if you haven't already linked it.
- Choose your previously created project from the list, or create a new one.
- Select a project template (e.g., "Clean project with a GraphQL API" or "Blog (schema and example content)"). For this tutorial, a basic blog template is a good starting point.
- Specify the local folder name for your Sanity Studio (e.g.,
sanity-studio).
Once initialized, navigate into your Sanity Studio directory and start the development server:
cd sanity-studio
npm install
sanity start
Your Sanity Studio project functions as your dedicated backend and content management system. Here, you define your content schemas and have full control to create and manage all your content. You can access it in your browser, usually at http://localhost:3333.
Step 2: Defining Your Content Schema
Now that your Sanity Studio is set up, you need to define the structure of your content. Sanity stands out as a developer-focused platform, allowing you to define your content schema—essentially a blueprint for your content—directly using JavaScript. This gives you granular control over your data model.
Inside your Sanity Studio project, navigate to the schemas folder (usually sanity-studio/schemas). You'll find a file like index.js that imports and exports all your schema types. Create a new file, for example, post.js, to define your blog post schema:
// sanity-studio/schemas/post.js
import { defineField, defineType } from 'sanity'
export default defineType({
name: 'post',
title: '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',
maxLength: 96,
},
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'author',
title: 'Author',
type: 'reference',
to: { type: 'author' }, // Assuming you have an 'author' schema
}),
defineField({
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
fields: [
defineField({
name: 'alt',
type: 'string',
title: 'Alternative Text',
}),
],
}),
defineField({
name: 'body',
title: 'Body',
type: 'array',
of: [
{
type: 'block', // Portable Text 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' }, { title: 'Numbered', value: 'number' }],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
],
annotations: [
{
name: 'link',
type: 'object',
title: 'URL',
fields: [
defineField({
title: 'URL',
name: 'href',
type: 'url',
}),
],
},
],
},
},
{
type: 'image',
options: { hotspot: true },
fields: [
defineField({
name: 'alt',
type: 'string',
title: 'Alternative text',
}),
],
},
],
}),
],
})
Remember to import and add post to your schemas/index.js file:
// sanity-studio/schemas/index.js
import post from './post'
import author from './author' // Assuming you have an author schema
export const schemaTypes = [post, author]
After saving, restart your Sanity Studio (sanity start) to see the new "Post" content type available for content creation.
Step 3: Connecting Your Next.js App to Sanity
Now, let's establish the connection between your Next.js frontend and your Sanity backend. Your Next.js project then acts as a completely separate frontend application, responsible for fetching this data from the Sanity backend using the Sanity client.
3.1 Create Your Next.js 14 Project
If you don't already have one, create a new Next.js 14 project:
npx create-next-app@latest my-nextjs-blog --typescript --tailwind --eslint
Navigate into your project directory:
cd my-nextjs-blog
3.2 Install the Sanity Client
Inside your Next.js project, install the necessary Sanity client libraries:
npm install next-sanity @portabletext/react @sanity/image-url
next-sanity: The core library for connecting Next.js to Sanity.@portabletext/react: For rendering Sanity's rich text (Portable Text) in React components.@sanity/image-url: For generating optimized image URLs from Sanity image assets.
3.3 Create the Sanity Client Configuration File
Create a file, for example, lib/sanity.client.ts (or .js), to configure your Sanity client. This file will hold your Sanity Project ID and dataset name.
// lib/sanity.client.ts
import { createClient } from 'next-sanity'
export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03' // Use a specific date for API version
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true, // `false` if you want to ensure fresh data
})
3.4 Handle Environment Variables
For security and flexibility, store your Sanity Project ID and dataset in environment variables. Create a .env.local file in the root of your Next.js project:
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="your-dataset-name"
NEXT_PUBLIC_SANITY_API_VERSION="2023-05-03"
You can find your Project ID and dataset name in your Sanity.io dashboard under your project settings. Remember to add .env.local to your .gitignore file.
Step 4: Fetching and Rendering Blog Posts
Now that your Next.js app is connected, let's fetch content using GROQ (Graph-Relational Object Queries) and display it. This is a core part of building a Next.js blog with Sanity.
4.1 Fetch All Blog Posts
In Next.js 14, you can fetch data directly within Server Components. Create a function to query all your blog posts:
// lib/sanity.queries.ts
import { groq } from 'next-sanity'
export const postsQuery = groq`
*[_type == "post"] | order(_createdAt desc) {
_id,
title,
slug,
mainImage {
asset->{
_id,
url
},
alt
},
author->{
name,
image
},
body,
_createdAt
}
`
Then, in your app/page.tsx (or .js) file, use this query with your Sanity client:
// app/page.tsx
import { client } from '@/lib/sanity.client'
import { postsQuery } from '@/lib/sanity.queries'
import { PortableText } from '@portabletext/react'
import imageUrlBuilder from '@sanity/image-url'
import Image from 'next/image'
import Link from 'next/link'
// Image builder for Sanity images
const builder = imageUrlBuilder(client)
function urlFor(source: any) {
return builder.image(source)
}
export default async function HomePage() {
const posts = await client.fetch(postsQuery)
if (!posts || posts.length === 0) {
return <div>No posts found.</div>
}
return (
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-8">Latest Blog Posts</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post: any) => (
<Link href={`/posts/${post.slug.current}`} key={post._id} className="block border rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300">
{post.mainImage && (
<Image
src={urlFor(post.mainImage).width(500).height(300).url()}
alt={post.mainImage.alt || post.title}
width={500}
height={300}
className="rounded-t-lg object-cover w-full h-48"
/>
)}
<div className="p-4">
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
{post.author && <p className="text-gray-600 text-sm">By {post.author.name}</p>}
{/* Optional: Render a short excerpt of the body */}
{post.body && post.body.length > 0 && (
<div className="text-gray-700 mt-2 line-clamp-3">
<PortableText value={post.body} />
</div>
)}
</div>
</Link>
))}
</div>
</div>
)
}
4.2 Rendering Rich Text (Portable Text)
Sanity's rich text is called Portable Text. To render it in your Next.js app, you use the PortableText component from @portabletext/react. You can define custom components for different block types (e.g., headings, lists, images within the text) to match your styling.
// components/PortableTextComponents.tsx (example)
import Image from 'next/image'
import { PortableTextComponents } from '@portabletext/react'
import imageUrlBuilder from '@sanity/image-url'
import { client } from '@/lib/sanity.client'
const builder = imageUrlBuilder(client)
function urlFor(source: any) {
return builder.image(source)
}
export const RichTextComponents: PortableTextComponents = {
types: {
image: ({ value }) => {
return (
<div className="relative w-full h-96 my-8">
<Image
className="object-contain"
src={urlFor(value).url()}
alt={value.alt || 'Blog Post Image'}
fill
priority
/>
</div>
)
},
},
list: {
bullet: ({ children }) => <ul className="ml-10 py-5 list-disc space-y-5">{children}</ul>,
number: ({ children }) => <ol className="mt-lg list-decimal">{children}</ol>,
},
block: {
h1: ({ children }) => <h1 className="text-5xl py-5 font-bold">{children}</h1>,
h2: ({ children }) => <h2 className="text-4xl py-5 font-bold">{children}</h2>,
h3: ({ children }) => <h3 className="text-3xl py-5 font-bold">{children}</h3>,
h4: ({ children }) => <h4 className="text-2xl py-5 font-bold">{children}</h4>,
blockquote: ({ children }) => <blockquote className="border-l-purple-500 border-l-4 pl-5 py-5 my-5">{children}</blockquote>,
},
marks: {
link: ({ children, value }) => {
const rel = !value.href.startsWith('/') ? 'noreferrer noopener' : undefined
return (
<Link href={value.href} rel={rel} className="underline decoration-purple-500 hover:decoration-black">
{children}
</Link>
)
},
},
}
You would then pass components={RichTextComponents} to your PortableText component.
Step 5: Creating Dynamic Routes for Individual Posts
To display each blog post on its own page, you'll use Next.js 14's dynamic routing feature. This allows you to create pages based on the slug of each post fetched from Sanity.
5.1 Create a Dynamic Route Folder Structure
Inside your app directory, create a folder named posts, and inside that, another folder named [slug]. Within [slug], create a page.tsx (or .js) file:
app/
└── posts/
└── [slug]/
└── page.tsx
5.2 Fetch a Single Blog Post by Slug
In lib/sanity.queries.ts, add a new query to fetch a single post:
// lib/sanity.queries.ts (add to existing queries)
export const postBySlugQuery = groq`
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
slug,
mainImage {
asset->{
_id,
url
},
alt
},
author->{
name,
image
},
body,
_createdAt
}
`
5.3 Implement the Dynamic Page
In your app/posts/[slug]/page.tsx, fetch the post using the slug from the URL parameters and render it:
// app/posts/[slug]/page.tsx
import { client } from '@/lib/sanity.client'
import { postBySlugQuery } from '@/lib/sanity.queries'
import { PortableText } from '@portabletext/react'
import imageUrlBuilder from '@sanity/image-url'
import Image from 'next/image'
import { RichTextComponents } from '@/components/PortableTextComponents' // Assuming you created this
// Image builder for Sanity images
const builder = imageUrlBuilder(client)
function urlFor(source: any) {
return builder.image(source)
}
interface PostPageProps {
params: {
slug: string
}
}
export default async function PostPage({ params }: PostPageProps) {
const post = await client.fetch(postBySlugQuery, { slug: params.slug })
if (!post) {
return <div>Post not found.</div>
}
return (
<article className="container mx-auto p-4 max-w-3xl">
<h1 className="text-5xl font-extrabold mb-4">{post.title}</h1>
{post.author && <p className="text-gray-600 text-lg mb-4">By {post.author.name}</p>}
{post.mainImage && (
<Image
src={urlFor(post.mainImage).width(800).height(450).url()}
alt={post.mainImage.alt || post.title}
width={800}
height={450}
className="rounded-lg object-cover w-full h-96 mb-8"
/>
)}
<div className="prose lg:prose-xl max-w-none">
<PortableText value={post.body} components={RichTextComponents} />
</div>
</article>
)
}
// Optional: Generate static paths for all posts at build time for better SEO and performance
export async function generateStaticParams() {
const slugs = await client.fetch(
groq`*[_type == "post" && defined(slug.current)][].slug.current`
)
return slugs.map((slug: string) => ({
slug,
}))
}
With these steps, you've successfully learned how to connect Sanity CMS with Next.js 14, enabling you to build powerful, content-driven applications. You can now create, manage, and display dynamic content with a flexible and performant stack.
Ready to level up your career?
Join 5 lakh+ learners on the Juno app. Certificate courses in Hindi and English.