A portfolio website is the single most important marketing asset for any developer or designer. Next.js gives you static generation for blazing-fast page loads, built-in image optimization, and server-side rendering for perfect SEO scores. This guide walks you through building a polished portfolio with a project showcase, animated transitions, and a working contact form.
Prerequisites
- →Node.js 20+
Next.js 15 requires Node.js 20 or later for the App Router and server components.
- →Basic React Knowledge
Familiarity with React components, props, and hooks like useState and useEffect.
- →A GitHub Account
You'll deploy to Vercel which connects directly to your GitHub repositories.
Create the Next.js Project with Tailwind CSS
Scaffold a new Next.js app with TypeScript and Tailwind CSS. The App Router gives you file-based routing and layouts out of the box, so each section of your portfolio gets its own route without manual configuration.
npx create-next-app@latest my-portfolio \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd my-portfolio
npm install framer-motionTip: Framer Motion adds smooth page transitions and scroll animations that make portfolios feel polished without heavy JavaScript.
Tip: Use the --src-dir flag to separate your source code from config files at the root.
Define Your Project Data as TypeScript Types
Create a typed data file for your projects instead of hardcoding them into components. This makes it trivial to add new projects later and keeps your components clean. Each project includes a title, description, tech stack tags, image path, and optional links to a live demo and source code.
// src/lib/projects.ts
export interface Project {
slug: string;
title: string;
description: string;
image: string;
tags: string[];
liveUrl?: string;
repoUrl?: string;
}
export const projects: Project[] = [
{
slug: "ecommerce-dashboard",
title: "E-Commerce Dashboard",
description:
"Real-time analytics dashboard with sales tracking, inventory management, and customer insights.",
image: "/projects/dashboard.png",
tags: ["Next.js", "TypeScript", "Prisma", "Tailwind CSS"],
liveUrl: "https://demo.example.com",
repoUrl: "https://github.com/you/dashboard",
},
{
slug: "ai-writing-tool",
title: "AI Writing Assistant",
description:
"AI-powered writing tool that generates blog posts, emails, and social media content from prompts.",
image: "/projects/ai-writer.png",
tags: ["React", "Node.js", "OpenAI", "PostgreSQL"],
liveUrl: "https://writer.example.com",
},
];Tip: Store project images in the public/projects/ directory so Next.js can serve them without import gymnastics.
Tip: Use slugs that match your project routes so linking between the card and detail page is automatic.
Build the Hero Section with Animated Introduction
Create a hero component with your name, title, and a brief tagline. Use Framer Motion to stagger the text entrance so elements fade in sequentially. This creates a memorable first impression without overwhelming the visitor.
// src/components/Hero.tsx
"use client";
import { motion } from "framer-motion";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.15 },
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5 } },
};
export default function Hero() {
return (
<section className="flex min-h-[80vh] flex-col justify-center px-6 md:px-16">
<motion.div
variants={container}
initial="hidden"
animate="show"
className="max-w-3xl"
>
<motion.p variants={item} className="text-sm font-mono text-emerald-400">
Hi, my name is
</motion.p>
<motion.h1
variants={item}
className="mt-4 text-5xl font-bold text-white md:text-7xl"
>
Your Name.
</motion.h1>
<motion.h2
variants={item}
className="mt-2 text-3xl font-bold text-gray-400 md:text-5xl"
>
I build things for the web.
</motion.h2>
<motion.p
variants={item}
className="mt-6 max-w-xl text-lg text-gray-400"
>
Full-stack developer specializing in building exceptional digital
experiences. Currently focused on building accessible, performant web
applications.
</motion.p>
</motion.div>
</section>
);
}Tip: Keep stagger timing between 0.1s and 0.2s — faster feels rushed, slower feels sluggish.
Tip: Test your hero on mobile first. A 7xl heading on desktop should drop to 4xl or 5xl on small screens.
Create the Project Showcase Grid
Build a responsive grid that displays your projects as cards with hover effects. Each card shows the project image, title, description, and tech stack tags. Use Next.js Image component for automatic optimization and lazy loading of project screenshots.
// src/components/ProjectCard.tsx
import Image from "next/image";
import Link from "next/link";
import type { Project } from "@/lib/projects";
export default function ProjectCard({ project }: { project: Project }) {
return (
<div className="group relative overflow-hidden rounded-lg border border-gray-800 bg-gray-900/50 transition-all hover:border-emerald-400/50">
<div className="relative aspect-video overflow-hidden">
<Image
src={project.image}
alt={project.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<div className="p-6">
<h3 className="text-xl font-semibold text-white">{project.title}</h3>
<p className="mt-2 text-sm text-gray-400">{project.description}</p>
<div className="mt-4 flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="rounded-full border border-gray-700 px-3 py-1 text-xs font-mono text-gray-300"
>
{tag}
</span>
))}
</div>
<div className="mt-4 flex gap-4">
{project.liveUrl && (
<Link
href={project.liveUrl}
target="_blank"
className="text-sm text-emerald-400 hover:underline"
>
Live Demo
</Link>
)}
{project.repoUrl && (
<Link
href={project.repoUrl}
target="_blank"
className="text-sm text-gray-400 hover:text-white"
>
Source Code
</Link>
)}
</div>
</div>
</div>
);
}Tip: Always provide the sizes prop to the Next.js Image component — without it, the browser downloads full-resolution images on every device.
Tip: Use aspect-video for project screenshots to keep the grid visually consistent even if image dimensions vary.
Add a Contact Form with Server Actions
Build a contact form using React Server Actions so submissions are handled server-side without a separate API route. The form collects a name, email, and message, validates the input, and sends the email using Resend. Server Actions keep the form logic colocated with the component.
// src/app/contact/actions.ts
"use server";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendMessage(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
if (!name || !email || !message) {
return { error: "All fields are required." };
}
try {
await resend.emails.send({
from: "Portfolio Contact <contact@yourdomain.com>",
to: "you@email.com",
subject: `New message from ${name}`,
text: `Name: ${name}\nEmail: ${email}\n\n${message}`,
});
return { success: true };
} catch {
return { error: "Failed to send message. Please try again." };
}
}Tip: Resend offers 100 free emails per day — more than enough for a portfolio contact form.
Tip: Add rate limiting in production to prevent form spam. A simple approach is to check the time since the last submission in a cookie.
Optimize SEO with Metadata and Open Graph Images
Configure metadata for every page using Next.js generateMetadata. Add Open Graph images so your portfolio looks polished when shared on Twitter, LinkedIn, and Slack. Next.js can generate OG images dynamically using the ImageResponse API, but for a portfolio, a well-designed static image works better.
// src/app/layout.tsx
import type { Metadata } from "next";
import { Space_Grotesk } from "next/font/google";
import "./globals.css";
const font = Space_Grotesk({ subsets: ["latin"] });
export const metadata: Metadata = {
metadataBase: new URL("https://yourname.dev"),
title: {
default: "Your Name — Full-Stack Developer",
template: "%s | Your Name",
},
description:
"Full-stack developer building fast, accessible web applications with Next.js, TypeScript, and modern tooling.",
openGraph: {
title: "Your Name — Full-Stack Developer",
description: "Full-stack developer specializing in Next.js and TypeScript.",
url: "https://yourname.dev",
siteName: "Your Name",
images: [
{
url: "/og.png",
width: 1200,
height: 630,
alt: "Your Name — Full-Stack Developer",
},
],
locale: "en_US",
type: "website",
},
robots: {
index: true,
follow: true,
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${font.className} bg-gray-950 text-white antialiased`}>
{children}
</body>
</html>
);
}Tip: Create your OG image at exactly 1200x630 pixels — this is the standard size for Twitter and LinkedIn previews.
Tip: Use the template property in metadata.title so subpages automatically get 'Page Title | Your Name' format.
Deploy to Vercel and Connect Your Domain
Push your code to GitHub and deploy to Vercel with zero configuration. Vercel detects Next.js automatically, builds your site, and gives you a preview URL. Connect a custom domain for a professional look. Every subsequent push to main triggers an automatic redeployment.
# Initialize git and push to GitHub
git init
git add .
git commit -m "Initial portfolio setup"
gh repo create my-portfolio --public --push
# Deploy to Vercel
npm install -g vercel
vercel
# Set environment variables for contact form
vercel env add RESEND_API_KEY
# Deploy to production
vercel --prodTip: Buy a .dev domain — they're affordable and signal that you're a developer.
Tip: Enable Vercel Analytics (free tier) to see which projects visitors click on most.
Next Steps
- →Add a blog section using MDX files for writing about your technical learnings.
- →Integrate a CMS like Sanity or Contentful so you can update projects without code changes.
- →Add dark/light theme toggle using next-themes for visitor preference.
- →Set up Vercel Speed Insights to monitor Core Web Vitals on your live portfolio.