How to Build a Modern Portfolio Blog with Next.js 16 and Sanity CMS
December 30, 2025

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:
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:
npx create-next-app@latest my-blog --typescript --tailwind --appcd 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:
npm install sanity next-sanity @sanity/image-url @portabletext/react
Creating the Sanity Client
In `sanity/lib/client.ts`, I configured the Sanity client:
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:
// 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`:
'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:
// app/blog/page.tsximport { 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:
// app/blog/[slug]/page.tsximport { 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:
// Using next-themes for state managementimport { 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:
: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:
// app/layout.tsxexport 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:
// app/blog/[slug]/page.tsxexport 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]!