Back to Blog
Next.jsWeb DevelopementSanity CMSTutorialNext.js+Sanity

How to Build a Modern Portfolio Blog with Next.js 16 and Sanity CMS

December 30, 2025

How to Build a Modern Portfolio Blog with Next.js 16 and Sanity CMS

A complete guide to creating a modern, fast, and SEO-friendly developer portfolio blog


Building a portfolio blog is one of the best investments you can make as a developer. It showcases your skills, helps you document your learning journey, and can even attract job opportunities. In this post, I'll walk you through exactly how I built this blog using Next.js 16, Sanity CMS, and Tailwind CSS.


Why I Chose This Tech Stack


Before diving into the implementation, let me explain why I selected these technologies:

Next.js 16 with App Router


Next.js has become the go-to React framework for production applications, and for good reason:


Server Components: Reduce JavaScript sent to the client, improving performance

Built-in SEO: Easy metadata management and automatic sitemap generation

Incremental Static Regeneration (ISR): Get the benefits of static sites with dynamic content updates

File-based Routing: Intuitive page organization with the App Router


Sanity CMS


For content management, I chose Sanity as my headless CMS:
Real-time Editing: See changes instantly in the embedded studio

Flexible Schemas: Define exactly how your content is structured

GROQ Queries: Powerful query language for fetching content

Free Tier: Generous free plan for personal projects

Image CDN: Automatic image optimization and delivery


Tailwind CSS v4


For styling, Tailwind CSS was the obvious choice:
Utility-First: Rapid development without leaving your HTML

Dark Mode: Built-in dark mode support with simple class prefixes

Responsive Design: Mobile-first breakpoints out of the box

Tiny Bundle: Only ships the CSS you actually use


Project Structure


Here's how I organized my project:

markdown
my-blog/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── blog/
│ │ ├── page.tsx
│ │ └── [slug]/page.tsx
│ ├── about/page.tsx
│ ├── projects/page.tsx
│ ├── components/
│ │ ├── Navbar.tsx
│ │ └── Footer.tsx
│ └── globals.css
├── sanity/
│ ├── lib/
│ │ ├── client.ts
│ │ └── image.ts
│ └── schemaTypes/
├── types/
│ └── blog.ts
└── package.json


Step 1: Setting Up Next.js


First, I created a new Next.js project with TypeScript:

bash
npx create-next-app@latest my-blog --typescript --tailwind --app
cd my-blog


This gives you a fresh Next.js 16 project with TypeScript and Tailwind CSS pre-configured.


Step 2: Integrating Sanity CMS


Installing Sanity


I added Sanity to my project with these packages:

bash
npm install sanity next-sanity @sanity/image-url @portabletext/react


Creating the Sanity Client


In `sanity/lib/client.ts`, I configured the Sanity client:

javascript
import { createClient } from 'next-sanity'
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: true,
})


Defining Content Schemas


The beauty of Sanity is its flexible schema system. Here's my blog post schema:

typescript
// sanity/schemaTypes/postType.tsimport { defineField, defineType } from'sanity'
import { defineField, defineType } from 'sanity'
export const postType = 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: 'author',
type: 'reference',
to: [{ type: 'author' }],
}),
defineField({
name: 'mainImage',
type: 'image',
options: { hotspot: true },
}),
defineField({
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
defineField({
name: 'publishedAt',
type: 'datetime',
}),
defineField({
name: 'body',
type: 'blockContent',
}),
],
})


Embedding Sanity Studio


One of my favorite features is embedding Sanity Studio directly in the Next.js app. I created a catch-all route at `app/studio/[[...tool]]/page.tsx`:

typescript
'use client'
import { NextStudio } from 'next-sanity/studio'
import config from '../../../sanity.config'
export default function StudioPage() {
return <NextStudio config={config} />
}


Now I can manage all my content at `/studio` without leaving my website!


Step 3: Building the Blog Pages


Blog Listing Page


The blog listing page fetches all posts and displays them in a responsive grid:

typescript
// app/blog/page.tsx
import { client } from '@/sanity/lib/client'
import Link from 'next/link'
import Image from 'next/image'
const query = `*[_type == "post"] | order(publishedAt desc) {
_id,
title,
slug,
mainImage,
publishedAt,
author->{name, image},
categories[]->{title}
}`
export const revalidate = 60 // ISR: revalidate every 60 seconds
export default async function BlogPage() {
const posts = await client.fetch(query)
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => (
<Link key={post._id} href={`/blog/${post.slug.current}`}>
{/* Post card content */}
</Link>
))}
</div>
)
}

Dynamic Blog Post Pages


For individual blog posts, I use dynamic routes with the `[slug]` folder:

typescript
// app/blog/[slug]/page.tsx
import { PortableText } from '@portabletext/react'
export default async function PostPage({ params }) {
const post = await client.fetch(
`*[_type == "post" && slug.current == $slug][0]{...}`,
{ slug: params.slug }
)
return (
<article>
<h1>{post.title}</h1>
<PortableText value={post.body} components={components} />
</article>
)
}

Step 4: Implementing Dark Mode


I added a smooth dark mode toggle with an animated transition:

typescript
// Using next-themes for state management
import { useTheme } from 'next-themes'
function ThemeToggle() {
const { theme, setTheme } = useTheme()
const toggleTheme = () => {
// Using View Transitions API for smooth animation
document.startViewTransition(() => {
setTheme(theme === 'dark' ? 'light' : 'dark')
})
}
return (
<button onClick={toggleTheme}>
{theme === 'dark' ? <SunIcon /> : <MoonIcon />}
</button>
)
}


The CSS uses the OKLCh color space for smooth color transitions:

css
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
}

Step 5: SEO Optimization

Metadata API


Next.js 16 makes SEO incredibly easy with the Metadata API:

typescript
// app/layout.tsx
export const metadata = {
title: 'Vishala Kothapally | Full Stack Developer',
description: 'Portfolio and blog of Vishala Kothapally...',
openGraph: {
title: 'Vishala Kothapally',
description: '...',
images: ['/og-image.png'],
},
}

Dynamic Metadata for Blog Posts


Each blog post generates its own metadata:

typescript
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [urlFor(post.mainImage).url()],
},
}
}

Key Features I Implemented

1. Responsive Design

Mobile-first approach with Tailwind breakpoints ensuring the blog looks great on all devices.

2. Image Optimization

Using Sanity's CDN with Next.js Image component for automatic optimization.

3. Fast Page Loads

Server Components reduce JavaScript bundle size, and ISR ensures content is always fresh.

4. Accessible Navigation

Semantic HTML, proper heading hierarchy, and keyboard-navigable menus.

5. Rich Text Rendering

Custom PortableText components for beautiful blog content with syntax highlighting.

Challenges and Solutions

Challenge 1: Theme Flash on Page Load

Problem: Users would see a flash of the wrong theme on page load.

Solution: Added an inline script in the HTML head to check localStorage before React hydrates.

Challenge 2: Image Sizing with Sanity

Problem: Images from Sanity didn't have consistent dimensions.

Solution: Used Sanity's image URL builder to request specific dimensions and aspect ratios.

Challenge 3: Mobile Navigation

Problem: Needed a smooth mobile menu that doesn't break scroll position.

Solution: Used Framer Motion for animations and proper scroll locking.

Performance Results


After building the blog, here are the Lighthouse scores:


Performance: 95+

Accessibility: 100

Best Practices: 100

SEO: 100

What's Next?


I'm planning to add:

- Comment system with reactions

- Newsletter subscription

- Reading time estimates

- Related posts recommendations

- Full-text search

Conclusion


Building a portfolio blog with Next.js 16 and Sanity CMS has been an excellent learning experience. The combination of server components, a flexible headless CMS, and utility-first CSS creates a developer experience that's both powerful and enjoyable.
If you're considering building your own portfolio blog, I highly recommend this stack. The initial setup takes a few hours, but you'll end up with a fast, SEO-friendly, and easily maintainable blog that you can grow with your career.

Have questions about building your own blog? Connect with me on [GitHub] or [LinkedIn]!