Why I Chose Next.js App Router for My Portfolio 15
A breakdown of why I went with Next.js App Router, Sanity, and GSAP instead of simpler alternatives — and what I learned along the way.
When I started planning my portfolio, the first decision was the tech stack. I could have gone with a simple HTML/CSS site or a template, but I wanted something that would grow with me — and something that would teach me production patterns I'd use in real projects.
The Case for App Router
Next.js 14 introduced a stable App Router with React Server Components, nested layouts, and streaming. For a portfolio, this means every page loads fast because most of the rendering happens on the server. The client only hydrates what it needs to — interactive components like animations and menus.
Server Components also make working with a CMS like Sanity effortless. You fetch data directly in the component, no useEffect or loading states needed:
// This runs on the server — zero client-side JavaScript
export default async function BlogPage() {
const posts = await getAllBlogPosts()
return (
<section>
{posts.map((post) => (
<BlogPostCard key={post._id} post={post} />
))}
</section>
)
}
Why Sanity Over Markdown
I considered using plain Markdown files for blog posts and project descriptions. It would have been simpler upfront — no CMS to configure, no schemas to write. But Sanity gives me structured content I can query with GROQ, a real-time preview studio, and the ability to add rich content like image galleries and code blocks without fighting a parser.
The schema-first approach also forces you to think about your content model before writing a single line of frontend code. Here's what the blog post schema looks like:
export const blogPost = defineType({
name: 'blogPost',
title: 'Blog Post',
type: 'document',
fields: [
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
defineField({ name: 'heroImage', type: 'image' }),
defineField({ name: 'body', type: 'blockContent' }),
],
})
Adding Motion with GSAP
In React, GSAP animations must be wrapped in a context and cleaned up when the component unmounts. Without this, you get memory leaks and ghost animations that fire on elements that no longer exist:
useEffect(() => {
const ctx = gsap.context(() => {
gsap.from('.card', {
y: 40,
opacity: 0,
stagger: 0.1,
duration: 0.6,
ease: 'power2.out',
})
}, containerRef)
return () => ctx.revert()
}, [])
What I Would Do Differently
If I started over, I'd set up the Sanity schemas and content model before writing any components. I spent time refactoring components when the data shape changed. Schema-first development saves that pain.
The best portfolio is one you actually ship. Perfect is the enemy of deployed.
Final Thoughts
Building a portfolio with Next.js App Router, Sanity, and GSAP taught me more about production web development than any course. Server components, content modeling, animation performance, deployment pipelines — these are the patterns you use in real projects. If you're thinking about building your own portfolio from scratch, I'd say go for it. The learning compounds.
