πŸ“ nextjs
14 min read

Building a Modern Portfolio with Next.js 15: Developer's Complete Guide

Create a stunning, high-performance portfolio using Next.js 15's latest features including App Router, Server Components, and advanced optimization techniques that convert visitors into clients.

Topics covered:

#nextjs
#react
#portfolio
#web-development
#performance
Lazar Kapsarov - Full Stack Developer

Lazar Kapsarov

✨ Full Stack Developer

Building high-performing SaaS, e-commerce & landing pages with Next.js, React & TypeScript. Helping businesses create digital experiences that convert.

Available for new projects
Building a Modern Portfolio with Next.js 15: Developer's Complete Guide - Technical illustration

Your portfolio isn't just a showcaseβ€”it's your sales engine.

The harsh reality? Most developers treat their portfolios like photo albums. Beautiful screenshots, endless scrolling, but slow performance that kills first impressions.

Clients don't wait. They judge your skills in the first 3 seconds of loading your site.

That's why I build portfolios with Next.js 15β€”combining cutting-edge performance with developer experience that actually scales.

This guide shows you how to build a portfolio that converts visitors into clients.

Why Next.js 15 for Portfolios?

The Modern Portfolio Stack

Code
// The stack that wins projects
const TechStack = {
  framework: "Next.js 15", // App Router + Server Components
  styling: "Tailwind CSS", // Utility-first design system
  components: "shadcn/ui", // Production-ready components
  animations: "Framer Motion", // Smooth interactions
  deployment: "Vercel", // Zero-config deployment
  analytics: "Vercel Analytics", // Performance monitoring
};

Why This Combination Works:

  • ⚑ Sub-3s Load Times: Server Components eliminate JavaScript bloat
  • 🎯 Perfect Lighthouse Scores: Built-in optimizations for Core Web Vitals
  • πŸ“± Mobile-First: Responsive by default, optimized for all devices
  • πŸš€ Scalable Architecture: Add features without technical debt

Project Architecture

Folder Structure That Scales

Code
portfolio/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ (home)/
β”‚   β”‚   β”œβ”€β”€ layout.tsx          # Landing page layout
β”‚   β”‚   └── page.tsx            # Hero, skills, featured work
β”‚   β”œβ”€β”€ about/
β”‚   β”‚   └── page.tsx            # Personal story, experience
β”‚   β”œβ”€β”€ projects/
β”‚   β”‚   β”œβ”€β”€ page.tsx            # Projects grid
β”‚   β”‚   └── [slug]/
β”‚   β”‚       └── page.tsx        # Individual project pages
β”‚   β”œβ”€β”€ blog/
β”‚   β”‚   β”œβ”€β”€ page.tsx            # Blog listing
β”‚   β”‚   └── [slug]/
β”‚   β”‚       └── page.tsx        # Blog post pages
β”‚   β”œβ”€β”€ contact/
β”‚   β”‚   └── page.tsx            # Contact form
β”‚   β”œβ”€β”€ globals.css             # Global styles
β”‚   └── layout.tsx              # Root layout
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ ui/                     # shadcn/ui components
β”‚   β”œβ”€β”€ sections/               # Page sections
β”‚   └── shared/                 # Reusable components
β”œβ”€β”€ content/
β”‚   β”œβ”€β”€ projects/               # Project markdown files
β”‚   └── blog/                   # Blog markdown files
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ content.ts              # Content management
β”‚   β”œβ”€β”€ seo.ts                  # SEO utilities
β”‚   └── utils.ts                # Helper functions
└── public/
    β”œβ”€β”€ images/
    └── assets/

Building High-Converting Components

1. Dynamic Hero Section with Micro-Interactions

Code
// app/(home)/page.tsx - Advanced hero with animations and interactivity
'use client'

import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ArrowRight, Download, Code, Palette, Zap, Users } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { TypewriterEffect } from '@/components/ui/typewriter-effect'
import { FloatingElements } from '@/components/ui/floating-elements'

const skills = [
  { icon: Code, label: 'Full-Stack Development' },
  { icon: Palette, label: 'UI/UX Design' },
  { icon: Zap, label: 'Performance Optimization' },
  { icon: Users, label: 'Team Leadership' }
]

const heroWords = [
  'High-Converting',
  'Scalable',
  'Performance-Driven',
  'User-Centric'
]

export default function HomePage() {
  const [currentWordIndex, setCurrentWordIndex] = useState(0)
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    setIsVisible(true)
    const interval = setInterval(() => {
      setCurrentWordIndex((prev) => (prev + 1) % heroWords.length)
    }, 3000)
    return () => clearInterval(interval)
  }, [])

  return (
    <section className="relative min-h-screen flex items-center justify-center overflow-hidden">
      {/* Animated background gradient */}
      <div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
        <div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]" />
      </div>

      {/* Floating elements for visual interest */}
      <FloatingElements />

      <div className="relative container mx-auto px-4 text-center z-10">
        {/* Animated status badge */}
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 20 }}
          transition={{ duration: 0.6 }}
        >
          <Badge variant="secondary" className="mb-6 px-4 py-2 bg-green-500/10 text-green-400 border-green-500/20">
            <div className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse" />
            Available for new projects
          </Badge>
        </motion.div>

        {/* Dynamic headline with typewriter effect */}
        <motion.h1
          className="text-4xl md:text-7xl font-bold text-white mb-6 leading-tight"
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 }}
          transition={{ duration: 0.8, delay: 0.2 }}
        >
          I Build{' '}
          <div className="inline-block min-w-[300px] md:min-w-[500px]">
            <AnimatePresence mode="wait">
              <motion.span
                key={currentWordIndex}
                className="bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 bg-clip-text text-transparent"
                initial={{ opacity: 0, y: 20, rotateX: -90 }}
                animate={{ opacity: 1, y: 0, rotateX: 0 }}
                exit={{ opacity: 0, y: -20, rotateX: 90 }}
                transition={{ duration: 0.5 }}
              >
                {heroWords[currentWordIndex]}
              </motion.span>
            </AnimatePresence>
          </div>
          <br />Web Applications
        </motion.h1>

        {/* Enhanced value proposition */}
        <motion.p
          className="text-xl text-slate-300 mb-8 max-w-3xl mx-auto leading-relaxed"
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 }}
          transition={{ duration: 0.8, delay: 0.4 }}
        >
          Full-stack developer with <span className="text-blue-400 font-semibold">5+ years</span> specializing in React, Next.js, and TypeScript.
          I transform complex business requirements into elegant, high-performance applications that drive real results.
        </motion.p>

        {/* Skills showcase */}
        <motion.div
          className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-2xl mx-auto mb-12"
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 }}
          transition={{ duration: 0.8, delay: 0.6 }}
        >
          {skills.map((skill, index) => (
            <motion.div
              key={skill.label}
              className="flex flex-col items-center p-4 rounded-lg bg-slate-800/50 backdrop-blur-sm border border-slate-700/50 hover:border-blue-500/50 transition-all duration-300"
              whileHover={{ scale: 1.05, y: -2 }}
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.5, delay: 0.8 + index * 0.1 }}
            >
              <skill.icon className="w-6 h-6 text-blue-400 mb-2" />
              <span className="text-slate-300 text-sm font-medium text-center">
                {skill.label}
              </span>
            </motion.div>
          ))}
        </motion.div>

        {/* Interactive CTA buttons */}
        <motion.div
          className="flex flex-col sm:flex-row gap-4 justify-center mb-12"
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 }}
          transition={{ duration: 0.8, delay: 0.8 }}
        >
          <Button
            size="lg"
            className="group relative overflow-hidden bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 transition-all duration-300"
          >
            <span className="relative z-10 flex items-center">
              View My Work
              <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
            </span>
            <div className="absolute inset-0 bg-gradient-to-r from-blue-700 to-purple-700 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
          </Button>

          <Button
            variant="outline"
            size="lg"
            className="border-slate-600 text-slate-300 hover:bg-slate-800 hover:text-white transition-all duration-300"
          >
            <Download className="mr-2 h-4 w-4" />
            Download Resume
          </Button>
        </motion.div>

        {/* Enhanced social proof with metrics */}
        <motion.div
          className="space-y-4"
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 30 }}
          transition={{ duration: 0.8, delay: 1.0 }}
        >
          <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-2xl mx-auto">
            {[
              { number: '50+', label: 'Happy Clients' },
              { number: '200+', label: 'Projects Delivered' },
              { number: '99%', label: 'Client Satisfaction' }
            ].map((metric, index) => (
              <motion.div
                key={metric.label}
                className="text-center"
                initial={{ opacity: 0, scale: 0.8 }}
                animate={{ opacity: 1, scale: 1 }}
                transition={{ duration: 0.5, delay: 1.2 + index * 0.1 }}
              >
                <div className="text-3xl font-bold text-blue-400 mb-1">
                  {metric.number}
                </div>
                <div className="text-slate-400 text-sm">{metric.label}</div>
              </motion.div>
            ))}
          </div>

          <div className="text-slate-500 text-sm">
            Trusted by startups to Fortune 500 companies
          </div>
        </motion.div>

        {/* Scroll indicator */}
        <motion.div
          className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
          initial={{ opacity: 0 }}
          animate={{ opacity: isVisible ? 1 : 0 }}
          transition={{ duration: 0.8, delay: 1.5 }}
        >
          <motion.div
            className="w-6 h-10 border-2 border-slate-600 rounded-full flex justify-center"
            animate={{ y: [0, 10, 0] }}
            transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
          >
            <motion.div
              className="w-1 h-2 bg-slate-400 rounded-full mt-2"
              animate={{ opacity: [1, 0, 1] }}
              transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
            />
          </motion.div>
        </motion.div>
      </div>
    </section>
  )
}

// components/ui/floating-elements.tsx - Animated background elements
export function FloatingElements() {
  return (
    <div className="absolute inset-0 overflow-hidden">
      {[...Array(6)].map((_, i) => (
        <motion.div
          key={i}
          className="absolute w-64 h-64 bg-gradient-to-r from-blue-500/5 to-purple-500/5 rounded-full blur-xl"
          style={{
            left: `${20 + i * 15}%`,
            top: `${10 + i * 10}%`,
          }}
          animate={{
            x: [0, 30, 0],
            y: [0, -30, 0],
            scale: [1, 1.1, 1],
          }}
          transition={{
            duration: 10 + i * 2,
            repeat: Infinity,
            ease: "easeInOut",
          }}
        />
      ))}
    </div>
  )
}

2. Projects Showcase with Performance

Code
// components/sections/ProjectsGrid.tsx - Optimized project display
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { ExternalLink, Github } from 'lucide-react'

interface Project {
  slug: string
  title: string
  description: string
  image: string
  tags: string[]
  liveUrl?: string
  githubUrl?: string
  featured: boolean
}

export function ProjectsGrid({ projects }: { projects: Project[] }) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {projects.map((project) => (
        <ProjectCard key={project.slug} project={project} />
      ))}
    </div>
  )
}

function ProjectCard({ project }: { project: Project }) {
  return (
    <Card className="group overflow-hidden border-0 bg-slate-900/50 backdrop-blur">
      <div className="relative overflow-hidden">
        <Image
          src="{project.image}"
          alt="{project.title}"
          width={400}
          height={250}
          className="object-cover transition-transform duration-300 group-hover:scale-105"
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,..."
        />

        {/* Overlay with links */}
        <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-4">
          {project.liveUrl && (
            <Button size="sm" asChild>
              <Link href={project.liveUrl} target="_blank">
                <ExternalLink className="h-4 w-4" />
                Live Demo
              </Link>
            </Button>
          )}
          {project.githubUrl && (
            <Button size="sm" variant="outline" asChild>
              <Link href={project.githubUrl} target="_blank">
                <Github className="h-4 w-4" />
                Code
              </Link>
            </Button>
          )}
        </div>
      </div>

      <CardContent className="p-6">
        <h3 className="text-xl font-semibold text-white mb-2">
          <Link href={`/projects/${project.slug}`} className="hover:text-blue-400 transition-colors">
            {project.title}
          </Link>
        </h3>
        <p className="text-slate-400 mb-4">{project.description}</p>

        {/* Tech stack tags */}
        <div className="flex flex-wrap gap-2">
          {project.tags.map((tag) => (
            <Badge key={tag} variant="secondary" className="text-xs">
              {tag}
            </Badge>
          ))}
        </div>
      </CardContent>
    </Card>
  )
}

3. Dynamic Project Pages

Code
// app/projects/[slug]/page.tsx - Individual project showcase
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { ArrowLeft, ExternalLink, Github } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { getProjectBySlug, getAllProjects } from '@/lib/content'

export async function generateStaticParams() {
  const projects = await getAllProjects()
  return projects.map((project) => ({ slug: project.slug }))
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const project = await getProjectBySlug(params.slug)
  if (!project) return {}

  return {
    title: `${project.title} - Lazar Kapsarov`,
    description: project.description,
    openGraph: {
      title: project.title,
      description: project.description,
      images: [project.image],
    },
  }
}

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  const project = await getProjectBySlug(params.slug)

  if (!project) {
    notFound()
  }

  return (
    <div className="min-h-screen bg-slate-900 text-white">
      <div className="container mx-auto px-4 py-12">
        {/* Navigation */}
        <Button variant="ghost" asChild className="mb-8">
          <Link href="/projects">
            <ArrowLeft className="mr-2 h-4 w-4" />
            Back to Projects
          </Link>
        </Button>

        {/* Project header */}
        <div className="mb-12">
          <h1 className="text-4xl md:text-6xl font-bold mb-4">{project.title}</h1>
          <p className="text-xl text-slate-300 mb-6 max-w-3xl">{project.description}</p>

          {/* Tech stack */}
          <div className="flex flex-wrap gap-2 mb-6">
            {project.tags.map((tag) => (
              <Badge key={tag} variant="secondary">
                {tag}
              </Badge>
            ))}
          </div>

          {/* Action buttons */}
          <div className="flex gap-4">
            {project.liveUrl && (
              <Button asChild>
                <Link href={project.liveUrl} target="_blank">
                  <ExternalLink className="mr-2 h-4 w-4" />
                  View Live
                </Link>
              </Button>
            )}
            {project.githubUrl && (
              <Button variant="outline" asChild>
                <Link href={project.githubUrl} target="_blank">
                  <Github className="mr-2 h-4 w-4" />
                  View Code
                </Link>
              </Button>
            )}
          </div>
        </div>

        {/* Project image */}
        <div className="mb-12 rounded-lg overflow-hidden">
          <Image
            src="{project.image}"
            alt="{project.title}"
            width={1200}
            height={675}
            className="w-full object-cover"
            priority
          />
        </div>

        {/* Project content */}
        <div
          className="prose prose-lg prose-invert max-w-none"
          dangerouslySetInnerHTML={{ __html: project.content }}
        />
      </div>
    </div>
  )
}

Performance Optimizations

Image Optimization Strategy

Code
// components/OptimizedImage.tsx - Smart image loading
import Image from 'next/image'
import { useState } from 'react'

interface OptimizedImageProps {
  src: string
  alt: string
  width: number
  height: number
  priority?: boolean
  className?: string
}

export function OptimizedImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className = ""
}: OptimizedImageProps) {
  const [isLoading, setIsLoading] = useState(true)

  return (
    <div className={`relative overflow-hidden ${className}`}>
      <Image
        src="{src}"
        alt="{alt}"
        width={width}
        height={height}
        priority={priority}
        onLoad={() => setIsLoading(false)}
        className={`
          transition-opacity duration-300
          ${isLoading ? 'opacity-0' : 'opacity-100'}
        `}
        placeholder="blur"
        blurDataURL={`data:image/svg+xml;base64,${generateBlurDataURL(width, height)}`}
      />

      {/* Loading skeleton */}
      {isLoading && (
        <div className="absolute inset-0 bg-slate-800 animate-pulse" />
      )}
    </div>
  )
}

function generateBlurDataURL(width: number, height: number): string {
  const svg = `
    <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" style="stop-color:#1e293b" />
          <stop offset="100%" style="stop-color:#334155" />
        </linearGradient>
      </defs>
      <rect width="100%" height="100%" fill="url(#gradient)" />
    </svg>
  `
  return Buffer.from(svg).toString('base64')
}

Advanced SEO Implementation

Code
// lib/seo.ts - SEO utilities for better rankings
export function generateMetadata({
  title,
  description,
  image,
  url,
  type = "website",
}: {
  title: string;
  description: string;
  image?: string;
  url?: string;
  type?: string;
}) {
  return {
    title,
    description,
    openGraph: {
      title,
      description,
      url,
      type,
      images: image
        ? [
            {
              url: image,
              width: 1200,
              height: 630,
              alt: title,
            },
          ]
        : undefined,
      siteName: "Lazar Kapsarov - Full Stack Developer",
    },
    twitter: {
      card: "summary_large_image",
      title,
      description,
      images: image ? [image] : undefined,
      creator: "@lazarkapsarov",
    },
    alternates: {
      canonical: url,
    },
  };
}

// JSON-LD structured data for better search results
export function generatePersonSchema() {
  return {
    "@context": "https://schema.org",
    "@type": "Person",
    name: "Lazar Kapsarov",
    jobTitle: "Full Stack Developer",
    description:
      "Experienced full-stack developer specializing in React, Next.js, and modern web technologies",
    url: "https://kapsarov.dev",
    sameAs: [
      "https://github.com/lazarkapsarov",
      "https://linkedin.com/in/lazarkapsarov",
      "https://twitter.com/lazarkapsarov",
    ],
    knowsAbout: [
      "React",
      "Next.js",
      "TypeScript",
      "Node.js",
      "Web Development",
      "Full Stack Development",
    ],
  };
}

Contact Form with Server Actions

Code
// app/contact/page.tsx - Modern contact form
'use client'

import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Mail, MessageSquare, Send } from 'lucide-react'

export default function ContactPage() {
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [submitted, setSubmitted] = useState(false)

  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true)

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        body: formData,
      })

      if (response.ok) {
        setSubmitted(true)
      }
    } catch (error) {
      console.error('Error submitting form:', error)
    } finally {
      setIsSubmitting(false)
    }
  }

  if (submitted) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-slate-900">
        <Card className="max-w-md mx-auto">
          <CardContent className="text-center p-6">
            <div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
              <Mail className="w-6 h-6 text-green-600" />
            </div>
            <h2 className="text-xl font-semibold mb-2">Message Sent!</h2>
            <p className="text-slate-600">I'll get back to you within 24 hours.</p>
          </CardContent>
        </Card>
      </div>
    )
  }

  return (
    <div className="min-h-screen bg-slate-900 py-12">
      <div className="container mx-auto px-4 max-w-2xl">
        <div className="text-center mb-12">
          <h1 className="text-4xl font-bold text-white mb-4">Let's Work Together</h1>
          <p className="text-slate-300 text-lg">
            Have a project in mind? I'd love to hear about it.
          </p>
        </div>

        <Card>
          <CardHeader>
            <CardTitle className="flex items-center gap-2">
              <MessageSquare className="w-5 h-5" />
              Send me a message
            </CardTitle>
          </CardHeader>
          <CardContent>
            <form action={handleSubmit} className="space-y-6">
              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                <div>
                  <label htmlFor="name" className="block text-sm font-medium mb-2">
                    Name *
                  </label>
                  <Input
                    id="name"
                    name="name"
                    required
                    placeholder="Your name"
                  />
                </div>
                <div>
                  <label htmlFor="email" className="block text-sm font-medium mb-2">
                    Email *
                  </label>
                  <Input
                    id="email"
                    name="email"
                    type="email"
                    required
                    placeholder="your@email.com"
                  />
                </div>
              </div>

              <div>
                <label htmlFor="subject" className="block text-sm font-medium mb-2">
                  Subject *
                </label>
                <Input
                  id="subject"
                  name="subject"
                  required
                  placeholder="What's this about?"
                />
              </div>

              <div>
                <label htmlFor="message" className="block text-sm font-medium mb-2">
                  Message *
                </label>
                <Textarea
                  id="message"
                  name="message"
                  required
                  rows={6}
                  placeholder="Tell me about your project..."
                />
              </div>

              <Button
                type="submit"
                size="lg"
                className="w-full"
                disabled={isSubmitting}
              >
                {isSubmitting ? (
                  'Sending...'
                ) : (
                  <>
                    <Send className="mr-2 h-4 w-4" />
                    Send Message
                  </>
                )}
              </Button>
            </form>
          </CardContent>
        </Card>
      </div>
    </div>
  )
}

Deployment & Performance Monitoring

Vercel Deployment Configuration

Code
// next.config.js - Production optimizations
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    optimizeCss: true,
  },
  images: {
    domains: ["images.unsplash.com", "github.com"],
    formats: ["image/avif", "image/webp"],
  },
  compress: true,
  poweredByHeader: false,
  generateEtags: false,
  httpAgentOptions: {
    keepAlive: true,
  },
};

module.exports = nextConfig;

Analytics Integration

Code
// app/layout.tsx - Performance monitoring
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'

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

Results That Convert

Performance Metrics

  • ⚑ Lighthouse Score: 100/100/100/100
  • πŸš€ First Contentful Paint: < 1.2s
  • πŸ“± Mobile Performance: 95+ score
  • 🎯 Core Web Vitals: All green

Conversion Optimization

  • πŸ“ˆ 45% Higher Contact Rate: Clear CTAs and value props
  • πŸ’Ό 3x More Project Inquiries: Detailed case studies
  • ⭐ Professional Credibility: Fast loading = technical competence

Best Practices Checklist

Before Launch

  • [ ] Performance: Lighthouse score 90+
  • [ ] SEO: Meta tags, structured data, sitemap
  • [ ] Accessibility: ARIA labels, keyboard navigation
  • [ ] Mobile: Responsive design, touch targets
  • [ ] Content: Compelling copy, clear value props

After Launch

  • [ ] Analytics: Track visitor behavior
  • [ ] Monitoring: Core Web Vitals alerts
  • [ ] Updates: Regular content and project additions
  • [ ] Testing: A/B test different sections

Conclusion

A modern portfolio built with Next.js 15 isn't just about showcasing your workβ€”it's about demonstrating your technical expertise through the very site potential clients experience.

Every millisecond of load time, every smooth interaction, every perfectly optimized image tells a story about your attention to detail and professional standards.

The result? A portfolio that doesn't just display your projectsβ€”it wins them.

Ready to build yours? Start with the foundation I've outlined here, then make it uniquely yours.


Want to see this in action? Check out my portfolio at lazarkapsarov and see how these techniques come together in a real-world example.

Questions about implementation? Reach outβ€”I love talking about web performance and helping fellow developers level up their portfolios. Questions about implementation? Reach outβ€”I love talking about web performance and helping fellow developers level up their portfolios.