Next.js

How to Build a Portfolio Website with Next.js

A step-by-step guide to building a fast, SEO-friendly developer portfolio with Next.js — from project setup to deployment.

June 13, 202610 min read
Share
Advertisement (not configured)

Introduction

A portfolio website is the first thing recruiters and clients check before they read your resume. Next.js is the best framework for building one — it gives you fast page loads, built-in SEO support, and easy deployment, all without extra configuration.

In this guide, we'll build a complete portfolio site with:

  1. Project setup with the App Router
  2. A homepage with hero, about, and skills sections
  3. A dynamic projects page
  4. A contact form
  5. SEO metadata
  6. Deployment to Vercel

By the end, you'll have a production-ready portfolio you can customize and ship.

1. Project Setup

Create a new Next.js app with the App Router and Tailwind CSS:

npx create-next-app@latest my-portfolio --typescript --tailwind --app
cd my-portfolio

Your folder structure should look like this:

app/
  layout.tsx
  page.tsx
  projects/
    page.tsx
  contact/
    page.tsx
components/
  Hero.tsx
  About.tsx
  Skills.tsx
  ProjectCard.tsx
data/
  projects.ts

2. Building the Homepage

Start with a hero section that introduces you:

// components/Hero.tsx
export default function Hero() {
  return (
    <section className="flex flex-col items-center justify-center min-h-[70vh] text-center px-4">
      <h1 className="text-4xl md:text-6xl font-bold">
        Hi, I'm Usman — Full-Stack Developer
      </h1>
      <p className="mt-4 text-lg text-gray-500 max-w-xl">
        I build fast, scalable web applications with Next.js, Node.js, and Python.
      </p>
      <a
        href="/projects"
        className="mt-6 px-6 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition"
      >
        View My Work
      </a>
    </section>
  );
}

Add an about section and a skills grid:

// components/Skills.tsx
const skills = [
  "Next.js", "React", "TypeScript", "Node.js", "Python", "PostgreSQL",
];

export default function Skills() {
  return (
    <section className="py-16 px-4">
      <h2 className="text-2xl font-bold mb-6 text-center">Skills</h2>
      <div className="flex flex-wrap gap-3 justify-center max-w-2xl mx-auto">
        {skills.map((skill) => (
          <span
            key={skill}
            className="px-4 py-2 bg-gray-100 rounded-full text-sm font-medium"
          >
            {skill}
          </span>
        ))}
      </div>
    </section>
  );
}

Wire it all together on the homepage:

// app/page.tsx
import Hero from "@/components/Hero";
import About from "@/components/About";
import Skills from "@/components/Skills";

export default function HomePage() {
  return (
    <main>
      <Hero />
      <About />
      <Skills />
    </main>
  );
}

3. Dynamic Projects Page

Store your projects as data instead of hardcoding markup — it's easier to update and reuse:

// data/projects.ts
export interface Project {
  slug: string;
  title: string;
  description: string;
  tech: string[];
  link: string;
}

export const projects: Project[] = [
  {
    slug: "task-manager",
    title: "Task Manager App",
    description: "A full-stack task management app with auth and real-time updates.",
    tech: ["Next.js", "Prisma", "PostgreSQL"],
    link: "https://github.com/yourname/task-manager",
  },
  {
    slug: "blog-platform",
    title: "Blog Platform",
    description: "A markdown-based blog platform with SEO optimization and dark mode.",
    tech: ["Next.js", "MDX", "Tailwind"],
    link: "https://github.com/yourname/blog-platform",
  },
];

Render the projects with a reusable card component:

// components/ProjectCard.tsx
import { Project } from "@/data/projects";

export default function ProjectCard({ project }: { project: Project }) {
  return (
    <a
      href={project.link}
      target="_blank"
      rel="noopener noreferrer"
      className="block p-6 border rounded-xl hover:shadow-md transition"
    >
      <h3 className="text-xl font-semibold">{project.title}</h3>
      <p className="mt-2 text-gray-500">{project.description}</p>
      <div className="mt-4 flex gap-2 flex-wrap">
        {project.tech.map((t) => (
          <span key={t} className="text-xs px-2 py-1 bg-gray-100 rounded">
            {t}
          </span>
        ))}
      </div>
    </a>
  );
}
// app/projects/page.tsx
import { projects } from "@/data/projects";
import ProjectCard from "@/components/ProjectCard";

export default function ProjectsPage() {
  return (
    <main className="max-w-4xl mx-auto px-4 py-16">
      <h1 className="text-3xl font-bold mb-8">Projects</h1>
      <div className="grid gap-6 md:grid-cols-2">
        {projects.map((project) => (
          <ProjectCard key={project.slug} project={project} />
        ))}
      </div>
    </main>
  );
}

4. Contact Form

A simple contact form using a Server Action — no external API needed:

// app/contact/page.tsx
async function sendMessage(formData: FormData) {
  "use server";
  const name = formData.get("name");
  const email = formData.get("email");
  const message = formData.get("message");

  // Send via email service (Resend, SendGrid, etc.)
  console.log({ name, email, message });
}

export default function ContactPage() {
  return (
    <main className="max-w-md mx-auto px-4 py-16">
      <h1 className="text-3xl font-bold mb-8">Get In Touch</h1>
      <form action={sendMessage} className="flex flex-col gap-4">
        <input
          name="name"
          placeholder="Your name"
          required
          className="border rounded-lg p-3"
        />
        <input
          name="email"
          type="email"
          placeholder="Your email"
          required
          className="border rounded-lg p-3"
        />
        <textarea
          name="message"
          placeholder="Your message"
          required
          rows={5}
          className="border rounded-lg p-3"
        />
        <button
          type="submit"
          className="bg-black text-white rounded-lg py-3 hover:bg-gray-800 transition"
        >
          Send Message
        </button>
      </form>
    </main>
  );
}

5. SEO Metadata

Next.js makes per-page metadata simple with the metadata export:

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Usman Zafar — Full-Stack Developer",
  description: "Portfolio of Usman Zafar, a full-stack developer specializing in Next.js, Node.js, and Python.",
  openGraph: {
    title: "Usman Zafar — Full-Stack Developer",
    description: "Check out my projects and get in touch.",
    url: "https://usmanzafar.dev",
    siteName: "Usman Zafar Portfolio",
  },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

For individual pages, export a metadata object the same way — Next.js merges it with the root layout's metadata automatically.

6. Deploying to Vercel

Push your code to GitHub, then connect the repo on vercel.com. Vercel auto-detects Next.js and deploys with zero configuration:

git add .
git commit -m "Initial portfolio"
git push origin main

Once connected, every push to main triggers a new production deployment. Add a custom domain from the Vercel dashboard under Settings → Domains.

Putting It All Together

Section Purpose
Hero First impression — name, role, call to action
About Brief background and what you do
Skills Quick scan of your tech stack
Projects Proof of work with live links
Contact Easy way for people to reach you

A portfolio doesn't need to be fancy — it needs to load fast, look clean, and clearly show what you've built. Start with this structure, swap in your own projects and copy, and deploy it today. You can always iterate once it's live.

Advertisement (not configured)

Written by

Raretechsol

International software company specializing in Python and JavaScript. Passionate about automation, AI, and building practical web applications.