Building a Blog with Next.js and Sanity: A Complete Step-by-Step Guide
Next.js and Sanity form a powerful combination for modern content-driven websites. Next.js provides a fast React-based frontend framework with server-side rendering and static generation capabilities, while Sanity offers a flexible headless CMS that allows content editors to manage content without touching code. In this tutorial, we will build a simple blog application from scratch, covering project setup, Sanity configuration, content schemas, data fetching, dynamic routes, and deployment-ready code examples.
Building a Blog with Next.js and Sanity: A Complete Step-by-Step Guide
Modern websites often require a clean separation between content management and frontend presentation. This is where a headless CMS shines.
One of the most popular stacks today combines:
- Next.js for the frontend
- Sanity CMS for content management
This setup allows developers to build highly performant websites while giving content editors a user-friendly interface to manage content.
In this tutorial, we'll build a simple blog using Next.js App Router and Sanity CMS.
---
Prerequisites
Before starting, ensure you have:
- Node.js 20+
- npm or pnpm
- A Sanity account
- Basic React knowledge
---
Step 1: Create a Next.js Project
Create a new Next.js application.
npx create-next-app@latest nextjs-sanity-demo
cd nextjs-sanity-demoRecommended options:
✔ TypeScript: Yes
✔ ESLint: Yes
✔ Tailwind CSS: Yes
✔ App Router: Yes
✔ src directory: YesStart the development server:
npm run devVisit:
http://localhost:3000---
Step 2: Create a Sanity Project
Install Sanity CLI:
npm create sanity@latestFollow the prompts:
Project Name: blog-cms
Dataset: production
Template: Clean projectAfter completion:
cd blog-cms
npm run devSanity Studio will be available at:
http://localhost:3333---
Step 3: Create a Post Schema
Inside:
sanity/schemaTypesCreate:
// post.ts
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string'
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title'
}
}),
defineField({
name: 'excerpt',
title: 'Excerpt',
type: 'text'
}),
defineField({
name: 'content',
title: 'Content',
type: 'array',
of: [{type: 'block'}]
})
]
})Register the schema:
// schema.ts
import post from './post'
export const schemaTypes = [post]Restart Studio.
You can now create blog posts from the Sanity dashboard.
---
Step 4: Install Sanity Client in Next.js
Back in the Next.js project:
npm install next-sanityCreate:
src/lib/sanity.tsimport { createClient } from 'next-sanity'
export const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-01-01',
useCdn: false
})Find your project ID in:
Sanity Dashboard → API---
Step 5: Create GROQ Queries
Create:
src/lib/queries.tsexport const POSTS_QUERY = `
*[_type == "post"]{
_id,
title,
slug,
excerpt
}
`Single post query:
export const POST_QUERY = `
*[_type == "post" && slug.current == $slug][0]
`---
Step 6: Fetch Posts on Homepage
Replace:
src/app/page.tsxwith:
import Link from 'next/link'
import { client } from '@/lib/sanity'
import { POSTS_QUERY } from '@/lib/queries'
export default async function Home() {
const posts = await client.fetch(POSTS_QUERY)
return (
<main className="max-w-4xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">
Blog
</h1>
{posts.map((post:any) => (
<article key={post._id} className="mb-6">
<Link
href={`/posts/${post.slug.current}`}
>
<h2 className="text-2xl font-semibold">
{post.title}
</h2>
</Link>
<p>{post.excerpt}</p>
</article>
))}
</main>
)
}---
Step 7: Create Dynamic Post Pages
Create:
src/app/posts/[slug]/page.tsximport { client } from '@/lib/sanity'
import { POST_QUERY } from '@/lib/queries'
export default async function PostPage({
params
}:{
params:{slug:string}
}) {
const post = await client.fetch(
POST_QUERY,
{ slug: params.slug }
)
return (
<article className="max-w-3xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-4">
{post.title}
</h1>
<p>{post.excerpt}</p>
</article>
)
}Now each blog post has its own route:
/posts/my-first-post---
Step 8: Render Rich Text Content
Install:
npm install @portabletext/reactUpdate:
import { PortableText } from '@portabletext/react'
<PortableText value={post.content} />Full example:
<article>
<h1>{post.title}</h1>
<PortableText
value={post.content}
/>
</article>This renders rich text created in Sanity Studio.
---
Step 9: Environment Variables
Create:
.env.localNEXT_PUBLIC_SANITY_PROJECT_ID=xxxxx
NEXT_PUBLIC_SANITY_DATASET=productionUpdate client:
export const client = createClient({
projectId:
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset:
process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2025-01-01',
useCdn: false
})Never hardcode project credentials.
---
Step 10: Deploy to Vercel
Push code:
git init
git add .
git commit -m "Initial commit"Create repository:
GitHubImport project into Vercel.
Add environment variables.
Deploy.
Your stack becomes:
Frontend:
Next.js (Vercel)
CMS:
Sanity Studio
Content API:
Sanity Content Lake---
Final Project Structure
src
├── app
│ ├── page.tsx
│ └── posts
│ └── [slug]
│ └── page.tsx
│
├── lib
│ ├── sanity.ts
│ └── queries.ts
sanity
├── schemaTypes
│ └── post.ts---
Why This Stack Is Popular
The Next.js + Sanity combination has become a favorite among startups, agencies, and content-driven businesses because it offers:
- Fast page performance
- Flexible content modeling
- Excellent SEO support
- Real-time content updates
- Easy deployment on Vercel
- Strong TypeScript support
- Scalability from small blogs to enterprise applications
For developers building blogs, documentation sites, company websites, educational platforms, or content-heavy SaaS products, Next.js and Sanity provide a modern and highly productive development experience.
About petercontinue
peter love study
Comments
Sign in to leave a comment.