作为一个开发者,你可能花了几百个小时学习新技术,却只用几行文字介绍自己?来,用 Next.js 14 + Tailwind CSS + shadcn/ui 搭一个专业的个人作品集网站,让你的技术实力"看得见"。
一、为什么你需要个人作品集网站?
1.1 作品集 vs 简历的对比
| 对比维度 | 简历 | 作品集网站 |
|---|---|---|
| 内容形式 | 静态文字 | 文字 + 图片 + 视频 + 交互 |
| 展示维度 | 扁平列表 | 视觉化 + 项目演示 + 动效 |
| 搜索引擎可见性 | 无 | SEO 友好 |
| 更新频率 | 每次求职重写 | 持续更新 |
| 成本 | 免费/模板 | 域名 + 托管 ≈ ¥100/年 |
| 印象分 | 中 | 高 |
1.2 为什么选这套技术栈?
Next.js 14 → React 全家桶,SSR/SSG 加持,SEO 无敌
Tailwind CSS → 原子化 CSS,写样式像搭积木
shadcn/ui → Radix UI 底层,专业组件库,设计感强
Vercel → 免费托管,一键部署,全球 CDN
二、环境准备
2.1 必要工具
# Node.js 20+ (建议使用 nvm 管理版本)
node --version # 确保 >= 20.0.0
# pnpm (比 npm 快 2-3 倍)
npm install -g pnpm
# Git (代码管理)
git --version
2.2 创建项目
# 使用 Next.js 官方脚手架
pnpm create next-app@latest portfolio
# 配置选项:
# - TypeScript: Yes
# - ESLint: Yes
# - Tailwind CSS: Yes
# - src/ directory: Yes
# - App Router: Yes ← 关键选项
# - Import alias: @/*
cd portfolio
pnpm dev # 启动开发服务器
2.3 项目结构
portfolio/
├── src/
│ ├── app/ # App Router
│ │ ├── page.tsx # 首页
│ │ ├── layout.tsx # 根布局
│ │ ├── globals.css # 全局样式
│ │ └── about/
│ │ └── page.tsx # 关于页
│ ├── components/
│ │ ├── ui/ # shadcn/ui 组件
│ │ ├── Hero.tsx # 首屏介绍
│ │ ├── Projects.tsx # 项目展示
│ │ ├── Skills.tsx # 技术栈
│ │ └── Contact.tsx # 联系方式
│ └── lib/
│ └── utils.ts # 工具函数
├── public/ # 静态资源
├── tailwind.config.ts # Tailwind 配置
├── next.config.js # Next.js 配置
└── package.json
三、安装与配置 shadcn/ui
3.1 初始化 shadcn/ui
# 在项目根目录执行
pnpm dlx shadcn@latest init
# 配置选项:
# - Style: Default
# - Base Color: Slate
# - CSS file: globals.css
# - CSS variables: Yes
# - Custom prefix: No
# - Components: .tsx
# - Utils: lib/utils.ts
# - ESLint: Yes
# - Tailwind: tailwind.config.ts
# - src/ directory: Yes
3.2 安装常用组件
# 按钮、卡片、导航栏、对话框、标签等
pnpm dlx shadcn@latest add button card navbar scroll-area badge avatar separator sheet
# 动画库 (Framer Motion)
pnpm add framer-motion
四、核心组件开发
4.1 Hero 首屏组件
// src/components/Hero.tsx
"use client";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { ArrowDown, Github, Linkedin, Mail } from "lucide-react";
export function Hero() {
return (
<section className="min-h-screen flex items-center justify-center px-6">
<div className="text-center max-w-3xl">
{/* 头像 */}
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="mb-8"
>
<div className="w-32 h-32 mx-auto rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-4xl font-bold text-white">
你
</div>
</motion.div>
{/* 标题 */}
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-4xl md:text-6xl font-bold mb-4"
>
Hi, I'm <span className="text-gradient">你的名字</span>
</motion.h1>
{/* 副标题 */}
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.3 }}
className="text-xl text-muted-foreground mb-8"
>
全栈开发者 | 开源贡献者 | 技术写作者
</motion.p>
{/* 社交链接 */}
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.4 }}
className="flex gap-4 justify-center mb-12"
>
<Button variant="outline" size="icon" asChild>
<a href="https://github.com/yourname" target="_blank" rel="noopener noreferrer">
<Github className="w-5 h-5" />
</a>
</Button>
<Button variant="outline" size="icon" asChild>
<a href="https://linkedin.com/in/yourname" target="_blank" rel="noopener noreferrer">
<Linkedin className="w-5 h-5" />
</a>
</Button>
<Button variant="outline" size="icon" asChild>
<a href="mailto:yourname@email.com">
<Mail className="w-5 h-5" />
</a>
</Button>
</motion.div>
{/* CTA 按钮 */}
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5 }}
className="flex gap-4 justify-center"
>
<Button size="lg" asChild>
<a href="#projects">查看项目</a>
</Button>
<Button size="lg" variant="secondary" asChild>
<a href="/resume.pdf">下载简历</a>
</Button>
</motion.div>
{/* 滚动提示 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ repeat: Infinity, duration: 2 }}
>
<ArrowDown className="w-6 h-6 text-muted-foreground" />
</motion.div>
</motion.div>
</div>
</section>
);
}
4.2 项目展示组件
// src/components/Projects.tsx
"use client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ExternalLink, Github } from "lucide-react";
import { motion } from "framer-motion";
const projects = [
{
title: "电商微服务系统",
description: "基于 Spring Cloud Alibaba 的分布式电商平台,支持高并发秒杀、分布式事务、灰度发布",
tags: ["Spring Boot", "Nacos", "Sentinel", "MySQL", "Redis"],
github: "https://github.com/yourname/ecommerce",
demo: "https://demo.92yangyi.top",
image: "/projects/ecommerce.jpg",
},
{
title: "AI 知识库问答系统",
description: "RAG 应用实战,集成 DeepSeek 大模型,支持文档上传、智能检索、溯源引用",
tags: ["Next.js", "LangChain", "Milvus", "DeepSeek", "Tailwind"],
github: "https://github.com/yourname/rag-chatbot",
demo: "https://chat.92yangyi.top",
image: "/projects/rag.jpg",
},
{
title: "开源组件库",
description: "封装了 20+ 常用业务组件,配套完整文档和 Demo,开源 Star 1000+",
tags: ["React", "TypeScript", "Storybook", "Vitest"],
github: "https://github.com/yourname/awesome-ui",
demo: "https://ui.92yangyi.top",
image: "/projects/ui.jpg",
},
];
export function Projects() {
return (
<section id="projects" className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">Featured Projects</h2>
<p className="text-muted-foreground">以下是一些我参与或主导的项目,持续更新中...</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project, index) => (
<motion.div
key={project.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
>
<Card className="h-full flex flex-col hover:shadow-lg transition-shadow">
{/* 项目图片 */}
<div className="aspect-video bg-gradient-to-br from-blue-100 to-purple-100 rounded-t-lg flex items-center justify-center">
<span className="text-6xl">🚀</span>
</div>
<CardHeader>
<CardTitle>{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</CardContent>
<CardFooter className="gap-2">
<Button variant="outline" size="sm" asChild>
<a href={project.github} target="_blank" rel="noopener noreferrer">
<Github className="w-4 h-4 mr-2" />
源码
</a>
</Button>
<Button size="sm" asChild>
<a href={project.demo} target="_blank" rel="noopener noreferrer">
<ExternalLink className="w-4 h-4 mr-2" />
演示
</a>
</Button>
</CardFooter>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
}
4.3 技术栈展示组件
// src/components/Skills.tsx
"use client";
import { motion } from "framer-motion";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const skillCategories = [
{
title: "后端开发",
icon: "⚙️",
skills: [
{ name: "Java / Spring Boot", level: 95 },
{ name: "Go / Gin", level: 70 },
{ name: "Python / FastAPI", level: 75 },
{ name: "数据库 / Redis", level: 85 },
],
},
{
title: "前端开发",
icon: "🎨",
skills: [
{ name: "React / Next.js", level: 85 },
{ name: "TypeScript", level: 88 },
{ name: "Tailwind CSS", level: 90 },
{ name: "Vue 3", level: 75 },
],
},
{
title: "DevOps",
icon: "☁️",
skills: [
{ name: "Docker / K8s", level: 80 },
{ name: "CI/CD (Jenkins/GitHub)", level: 85 },
{ name: "Linux", level: 85 },
{ name: "云服务 (AWS/阿里云)", level: 75 },
],
},
];
export function Skills() {
return (
<section id="skills" className="py-24 px-6 bg-muted/50">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">技术栈</h2>
<p className="text-muted-foreground">持续学习,与时俱进</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-6">
{skillCategories.map((category, catIndex) => (
<motion.div
key={category.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: catIndex * 0.1 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>{category.icon}</span>
{category.title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{category.skills.map((skill, skillIndex) => (
<div key={skill.name}>
<div className="flex justify-between mb-1">
<span className="text-sm">{skill.name}</span>
<span className="text-sm text-muted-foreground">{skill.level}%</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<motion.div
className="h-full bg-primary rounded-full"
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: skillIndex * 0.1 }}
/>
</div>
</div>
))}
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
}
4.4 联系方式组件
// src/components/Contact.tsx
"use client";
import { motion } from "framer-motion";
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, Github, MapPin } from "lucide-react";
export function Contact() {
return (
<section id="contact" className="py-24 px-6">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">联系我</h2>
<p className="text-muted-foreground">有项目合作或技术交流?欢迎联系我</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8">
{/* 联系信息 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
className="space-y-6"
>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-primary/10 rounded-lg">
<Mail className="w-6 h-6 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">邮箱</p>
<p className="font-medium">yourname@email.com</p>
</div>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-primary/10 rounded-lg">
<Github className="w-6 h-6 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">GitHub</p>
<p className="font-medium">@yourname</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg">
<MapPin className="w-6 h-6 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">位置</p>
<p className="font-medium">中国 · 上海</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* 联系表单 */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<Card>
<CardHeader>
<CardTitle>发送消息</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="name" className="text-sm">姓名</label>
<Input id="name" placeholder="你的名字" />
</div>
<div className="space-y-2">
<label htmlFor="email" className="text-sm">邮箱</label>
<Input id="email" type="email" placeholder="your@email.com" />
</div>
</div>
<div className="space-y-2">
<label htmlFor="message" className="text-sm">留言</label>
<Textarea id="message" placeholder="想和我说些什么..." className="min-h-[120px]" />
</div>
<Button className="w-full">发送消息</Button>
</form>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
);
}
五、导航栏组件
// src/components/Navbar.tsx
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Menu, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
const navItems = [
{ href: "#home", label: "首页" },
{ href: "#projects", label: "项目" },
{ href: "#skills", label: "技能" },
{ href: "#contact", label: "联系" },
];
export function Navbar() {
const [scrolled, setScrolled] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
className={`fixed top-0 left-0 right-0 z-50 transition-colors ${
scrolled ? "bg-background/80 backdrop-blur-md border-b" : "bg-transparent"
}`}
>
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */}
<a href="#home" className="font-bold text-xl">
Your<span className="text-primary">Logo</span>
</a>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
{navItems.map((item) => (
<a
key={item.href}
href={item.href}
className="text-sm font-medium hover:text-primary transition-colors"
>
{item.label}
</a>
))}
</nav>
{/* Right Section */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="w-5 h-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute w-5 h-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
{/* Mobile Menu */}
<Sheet>
<SheetTrigger asChild className="md:hidden">
<Button variant="ghost" size="icon">
<Menu className="w-5 h-5" />
</Button>
</SheetTrigger>
<SheetContent>
<nav className="flex flex-col gap-4 mt-8">
{navItems.map((item) => (
<a
key={item.href}
href={item.href}
className="text-lg font-medium hover:text-primary transition-colors"
>
{item.label}
</a>
))}
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</motion.header>
);
}
六、主题切换配置
6.1 安装 next-themes
pnpm add next-themes
6.2 根布局配置
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Navbar } from "@/components/Navbar";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "你的名字 | 全栈开发者",
description: "专注于全栈开发、微服务架构、AI 应用的技术博客",
keywords: ["开发者", "全栈", "Java", "React", "Next.js"],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Navbar />
<main>{children}</main>
</ThemeProvider>
</body>
</html>
);
}
七、首页组装
// src/app/page.tsx
import { Hero } from "@/components/Hero";
import { Projects } from "@/components/Projects";
import { Skills } from "@/components/Skills";
import { Contact } from "@/components/Contact";
export default function Home() {
return (
<>
<Hero />
<Projects />
<Skills />
<Contact />
</>
);
}
八、Tailwind 配置优化
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
// 自定义渐变色
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
// 动画
animation: {
"fade-in": "fadeIn 0.5s ease-in-out",
"slide-up": "slideUp 0.5s ease-out",
},
keyframes: {
fadeIn: {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
slideUp: {
"0%": { transform: "translateY(20px)", opacity: "0" },
"100%": { transform: "translateY(0)", opacity: "1" },
},
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;
九、部署到 Vercel
9.1 GitHub 推送
# 初始化 Git
git init
git add .
git commit -m "feat: 作品集网站 v1.0"
# 创建 GitHub 仓库并推送
gh repo create portfolio --public --push
9.2 Vercel 部署
方式一:命令行部署
# 安装 Vercel CLI
pnpm add -g vercel
# 登录并部署
cd portfolio
vercel
# 生产环境部署
vercel --prod
方式二:网页部署
- 访问 vercel.com
- 点击 "New Project"
- 导入你的 GitHub 仓库
- 点击 "Deploy"
- 自动构建并部署完成!
9.3 自定义域名(可选)
Vercel → 项目 → Settings → Domains → 添加你的域名
DNS 解析指向 Vercel 提供的 CNAME 记录
十、SEO 优化
10.1 metadata 配置
// src/app/layout.tsx
export const metadata: Metadata = {
title: {
default: "你的名字 | 全栈开发者",
template: "%s | 你的名字",
},
description: "专注于全栈开发、微服务架构、AI 应用的技术博客",
keywords: ["开发者", "全栈", "Java", "React", "Next.js", "技术博客"],
authors: [{ name: "你的名字" }],
creator: "你的名字",
openGraph: {
type: "website",
locale: "zh_CN",
url: "https://yourname.vercel.app",
siteName: "你的名字",
title: "你的名字 | 全栈开发者",
description: "专注于全栈开发、微服务架构、AI 应用的技术博客",
},
twitter: {
card: "summary_large_image",
title: "你的名字 | 全栈开发者",
description: "专注于全栈开发、微服务架构、AI 应用的技术博客",
},
};
10.2 sitemap 生成
pnpm add next-sitemap
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || "https://yourname.vercel.app",
generateRobotsTxt: true,
};
十一、效果预览
部署完成后,你的作品集网站将具备以下特性:
| 特性 | 实现 |
|---|---|
| 响应式设计 | ✅ 手机、平板、桌面完美适配 |
| 暗色模式 | ✅ 一键切换,自动跟随系统 |
| 流畅动画 | ✅ Framer Motion 赋能 |
| SEO 友好 | ✅ SSR + metadata 优化 |
| 快速加载 | ✅ Vercel 全球 CDN |
| 免费托管 | ✅ Vercel Hobby 计划 |
| 自定义域名 | ✅ 支持 |
十二、后续优化建议
12.1 功能增强
- 📝 添加博客功能(Next.js + MDX)
- 📊 添加访客统计(Vercel Analytics / Umami)
- 📬 添加邮件订阅(Resend / Mailchimp)
- 🤖 添加 AI 对话(DeepSeek API)
12.2 性能优化
# Lighthouse 性能检查
# - Core Web Vitals 全部绿标
# - LCP < 2.5s
# - FID < 100ms
# - CLS < 0.1
十三、完整代码获取
所有源码已开源,欢迎 Star:
GitHub: https://github.com/yourname/portfolio
在线演示: https://yourname.vercel.app
十四、结语
一个好的作品集网站,就是你 24/7 在线的"技术名片"。用 Next.js 14 + Tailwind CSS + shadcn/ui,你可以在一个周末内完成一个专业级的个人网站。
下一步行动:
- 按照本教程,从零搭建你的作品集
- 替换为你的真实项目和信息
- 部署到 Vercel
- 在社交媒体分享你的作品
期待看到你的作品集!
💬 评论区互动:你在搭建作品集时遇到过哪些问题?欢迎留言交流!
评论区