博客项目实战指南

Next.js 14 + MongoDB + TailwindCSS | TypeScript + 根目录布局 | 增量构建 · 每步可验证

TypeScript 根目录布局 增量构建 全栈评论系统 暗色主题 Vercel 部署
注意事项:本指南需配合以下两个文档操作:
📄 博客项目实战指南-从零搭建.md — 完整的从零开始搭建流程
📄 博客项目实战指南-适配引擎.md — AI 适配引擎专用配置说明
🧠 适配引擎 · 给AI的上下文提示

用途: 复制下方提示词,AI可精准生成代码;知识地图、诊断清单快速排查问题。

本版本修正: 所有代码使用 TypeScript(.tsx/.ts),目录结构为根目录布局(app/ 直接在项目根目录,无 src/ 前缀)。
一、技术栈
模块技术说明
框架Next.js 14 (App Router)React 全栈框架,支持 SSR/SSG
语言TypeScript类型安全,减少运行时错误
样式TailwindCSS原子化 CSS,支持暗色模式
数据库MongoDB + MongooseNoSQL 文档数据库
主题next-themes暗色/亮色主题切换
部署Vercel零配置部署平台
最终项目结构
my-blog/
├── app/
│ ├── globals.css
│ ├── layout.tsx                 ← 根布局
│ ├── page.tsx                   ← 首页
│ ├── providers.tsx             ← ThemeProvider
│ ├── about/page.tsx             ← 关于页
│ ├── posts/page.tsx             ← 文章列表
│ ├── posts/[slug]/page.tsx      ← 文章详情
│ └── api/comments/route.ts     ← 评论 API
├── components/
│ ├── Navbar.tsx, ThemeToggle.tsx
│ ├── CommentForm.tsx, CommentList.tsx
│ └── CommentSection.tsx
├── lib/mongodb.ts
├── models/Comment.ts
├── .env.local, .gitignore
├── package.json, tsconfig.json
├── tailwind.config.ts
└── next.config.mjs
二、🤖 AI 提示词模板

使用方式: 把你的网站所有文件和本指南一起发给 AI,然后发送下方提示词。

你已经收到了我的网站文件和一份《博客项目实战指南》。请立即执行以下操作,不要询问我更多信息,不要重写指南内容。

【第一步:读取我上传的文件】
直接读取你已经收到的所有文件,列出:
- 文件清单(文件名 + 文件类型)
- 技术栈判断(用了哪些技术:HTML/CSS/JS/框架/库)
- 项目环境判断(有没有 package.json / .git / node_modules)

【第二步:诊断】
对照指南中的「四、诊断清单」,逐项检查我的网站。输出表格:
| 编号 | 能力项 | 是否具备 | 你的依据 |
|------|--------|---------|---------|
| A1 | HTML 页面结构 | ✅/❌ | 写明你在文件中看到的具体证据 |
| ... | (全部18项都列出) | ... | ... |

【第三步:告诉我从哪步开始】
- 应该从步骤几开始(①②③④⑤⑥)
- 步骤①到我的入口步骤之前的那些步骤,可以跳过
- 一句话总结:"你的网站现状是xxx,需要从xxx开始"

【第四步:给我可执行的方案】
- 跳过已完成的步骤
- 未完成的步骤给出完整代码(TypeScript,根目录布局)
- 代码中需要我自己填写的部分,用 // TODO: 替换为你的xxx 标注
- 每步结尾给出检查点

注意:不要把指南原文重新输出。不要问我"请提供文件"。只输出诊断结果和执行方案。

AI工作流:读文档+代码 → 知识地图 → 诊断清单 → 迁移规则 → 代码模板 → 个性化适配方案(~70%可直接用)

三、📌 知识地图(能力表)

每项能力标注:编号 / 名称 / 依赖项 / 是否必须。19 项能力,4 层依赖。

基础层(必须)

编号能力依赖必须
A1HTML 页面结构
A2CSS 样式A1
A3JavaScript 交互A1, A2
A4ES6 模块化A3
A5Node.js 运行时
A6Git 版本控制

框架层(必须)

编号能力依赖必须
B1React 组件化A4
B2Next.js 路由B1
B3Next.js 数据获取B2
B4API RoutesB2
B5样式方案B2

功能层(推荐)

编号能力依赖必须
C1Tailwind CSSB5💡
C2第三方 React 组件B1
C3MongoDB 数据库B4💡
C4评论系统C3
C5昼夜模式B5

部署层(必须)

编号能力依赖必须
D1Git 推送到 GitHubA6
D2Vercel 部署D1
D3环境变量管理D2
四、🩺 诊断清单(18项)

逐项检查。勾选已有的,未勾选的就是需要补齐的。

基础检查

框架检查

功能检查

部署检查

完成进度:0/18
五、🧭 迁移路径
你的情况诊断特征入口步骤
静态 HTML 网站只有 HTML/CSS/JS步骤①
有 npm + Reactpackage.json + JSX步骤②
已有 Next.jsapp/page.tsx + 路由步骤③
已有 Next.js + APIroute.ts + 数据库步骤⑤
功能完整只想部署所有功能都有步骤⑥
路径图:
静态HTML → ① 初始化 → ② 搭骨架 → ③ 写页面 → ④ 接数据 → ⑤ 加功能 → ⑥ 部署
六、📋 代码模板库(13个)
编号文件用途步骤
1app/layout.tsx根布局
2components/Navbar.tsx导航栏
3app/posts/page.tsx文章列表
4app/posts/[slug]/page.tsx详情页
5lib/mongodb.ts数据库连接
6models/Comment.ts数据模型
7app/api/comments/route.ts评论API
8components/CommentForm.tsx评论表单
9components/CommentList.tsx评论列表
10components/CommentSection.tsx评论组合器
11components/ThemeToggle.tsx主题切换
12app/providers.tsxThemeProvider
13.env.local环境变量

AI使用提示: 将提示词 + 步骤编号发送给AI,按增量模式输出完整代码。

📦 基础搭建 · 初始化到页面开发

严格按顺序操作,每一步完成后都会展示"当前文件树" + "浏览器验证"。

1📦 初始化项目 & 安装依赖
npx create-next-app@latest my-blog --typescript --tailwind --eslint --app --no-src-dir
cd my-blog
npm install mongoose next-themes
npm run dev
关键参数: --typescript 启用 TS,--no-src-dir 根目录布局。
Tailwind 版本检查: npm ls tailwindcss。v3 用 @tailwind base;,v4 用 @import "tailwindcss";
✅ 验证:http://localhost:3000 看到 Next.js 欢迎页。
📁 步骤①完成后
my-blog/
├── package.json, tsconfig.json, tailwind.config.ts
├── app/ (globals.css, layout.tsx, page.tsx)
└── public/
2🏗️ 搭骨架:导航栏 & 全局布局

新增: Navbar.tsx, ThemeToggle.tsx, providers.tsx | 修改: layout.tsx, tailwind.config.ts

// components/Navbar.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import ThemeToggle from "./ThemeToggle";
export default function Navbar() {
  const pathname = usePathname();
  const navItems = [{ href: "/", label: "首页", icon: "🏠" }, { href: "/posts", label: "文章", icon: "📝" }, { href: "/about", label: "关于", icon: "👤" }];
  return (
    <nav className="sticky top-0 z-50 backdrop-blur-lg bg-white dark:bg-gray-900 border-b">
      <div className="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
        <Link href="/" className="flex items-center gap-2"><span className="text-2xl">🚀</span><span className="text-xl font-bold text-blue-700 dark:text-blue-400">My Blog</span></Link>
        <div className="flex items-center gap-1">
          {navItems.map((item) => (<Link key={item.href} href={item.href} className={`px-4 py-2 rounded-xl text-sm font-bold ${pathname === item.href ? "bg-gray-900 dark:bg-white text-white dark:text-black" : "hover:bg-gray-100 dark:hover:bg-gray-800"}`}>{item.icon} {item.label}</Link>))}
          <div className="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-2"></div><ThemeToggle />
        </div>
      </div>
    </nav>
  );
}
// components/ThemeToggle.tsx
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function ThemeToggle() {
  const [mounted, setMounted] = useState(false);
  const { theme, setTheme } = useTheme();
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return (<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")} className="p-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 border hover:border-blue-500" aria-label="切换主题">{theme === "dark" ? "☀️" : "🌙"}</button>);
}
// app/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
export default function Providers({ children }: { children: React.ReactNode }) {
  return (<ThemeProvider attribute="class" defaultTheme="system" enableSystem>{children}</ThemeProvider>);
}
// tailwind.config.ts - must change darkMode
import type { Config } from "tailwindcss";
const config: Config = {
  content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"],
  darkMode: "class",
  theme: { extend: {} }, plugins: [],
};
export default config;
暗色主题修正: 默认 darkMode: "media" 不响应 class,必须改 "class"
// app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import Navbar from "@/components/Navbar";
import Providers from "./providers";
export const metadata: Metadata = { title: "我的博客", description: "全栈博客" };
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (<html lang="zh" suppressHydrationWarning><body className="bg-gray-50 dark:bg-slate-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors"><Providers><Navbar /><main className="max-w-6xl mx-auto px-6 py-8">{children}</main></Providers></body></html>);
}
✅ 验证:导航栏可见,主题切换按钮可点击。
📁 步骤②完成后
├── app/ → globals.css, layout.tsx, page.tsx, providers.tsx
├── components/ → Navbar.tsx, ThemeToggle.tsx
└── tailwind.config.ts
3📄 页面开发:首页、列表、详情、关于

新增 4 个文件(模拟数据,后续接数据库)

// app/posts/page.tsx
import Link from "next/link";
const mockPosts = [
  { slug: "hello-world", title: "Hello World", date: "2025-01-01", desc: "第一篇博文", icon: "👋" },
  { slug: "nextjs-guide", title: "Next.js 入门", date: "2025-01-15", desc: "App Router 实战", icon: "⚡" },
];
export default function PostsPage() {
  return (<div><h1 className="text-3xl font-bold mb-8">📝 所有文章</h1><div className="grid gap-6">
    {mockPosts.map((p) => (<Link key={p.slug} href={`/posts/${p.slug}`} className="block p-6 bg-white dark:bg-gray-800 rounded-2xl border hover:shadow-xl hover:border-blue-500 transition-all group">
      <div className="flex items-start gap-4"><span className="text-4xl">{p.icon}</span><div className="flex-1"><h2 className="text-xl font-bold group-hover:text-blue-600">{p.title}</h2><p className="text-gray-500 mt-2">{p.desc}</p><p className="text-sm text-gray-400 mt-3">📅 {p.date}</p></div></div>
    </Link>))}</div></div>);
}
// app/posts/[slug]/page.tsx
import CommentSection from "@/components/CommentSection";
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return (<article className="max-w-4xl mx-auto">
    <header className="mb-8 pb-6 border-b border-gray-200 dark:border-gray-700">
      <h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">{slug}</h1>
      <div className="flex items-center gap-4 text-gray-500"><span>📅 2025-02-01</span><span>⏱️ 5 分钟</span></div>
    </header>
    <div className="prose prose-lg dark:prose-invert max-w-none mb-12"><p className="text-gray-600 dark:text-gray-300">这里是文章内容...</p></div>
    <CommentSection postId={slug} />
  </article>);
}
TypeScript fix: Next.js 15 params is Promise<{ slug: string }>, needs await.
// app/about/page.tsx
export default function AboutPage() {
  return (<div className="max-w-4xl mx-auto">
    <div className="text-center mb-12"><span className="text-7xl block mb-6">👤</span><h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">关于我</h1></div>
    <div className="bg-white dark:bg-gray-800 rounded-2xl p-8 border shadow-lg">
      <p className="text-lg text-gray-600 dark:text-gray-300 mb-6">Next.js + MongoDB 全栈博客系统。</p>
      <div className="grid grid-cols-2 gap-4">
        <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-xl text-center">⚡ Next.js 14</div>
        <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-xl text-center">🗄️ MongoDB</div>
        <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-xl text-center">🎨 TailwindCSS</div>
        <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-xl text-center">🌙 暗色主题</div>
      </div>
    </div>
  </div>);
}
// app/page.tsx
import Link from "next/link";
export default function HomePage() {
  return (<div className="text-center py-20">
    <span className="text-7xl animate-bounce inline-block mb-6">🚀</span>
    <h1 className="text-5xl font-bold mb-4 text-black dark:text-white">我的博客</h1>
    <p className="text-xl text-gray-700 dark:text-gray-300 mb-8">全栈博客 · Next.js + MongoDB</p>
    <Link href="/posts" className="inline-flex items-center gap-2 bg-blue-700 text-white px-8 py-4 rounded-2xl font-bold text-lg shadow-lg hover:bg-blue-800 transition-all hover:-translate-y-1">浏览文章 →</Link>
  </div>);
}
✅ 验证:/posts 看到列表,/posts/hello-world 看到详情。
📁 步骤③完成后
app/ → layout.tsx, page.tsx, providers.tsx, globals.css, posts/page.tsx, posts/[slug]/page.tsx, about/page.tsx
components/ → Navbar.tsx, ThemeToggle.tsx
🧩 功能增强 · 评论系统 + 暗色主题

在基础搭建完成后,继续添加评论系统和暗色主题功能。

4🗄️ 接入数据库 & 评论 API

新增: .env.local, lib/mongodb.ts, models/Comment.ts, app/api/comments/route.ts

# .env.local
MONGODB_URI=mongodb+srv://你的账号:***@cluster.mongodb.net/blog?retryWrites=true&w=majority
DB_NAME=blog
// lib/mongodb.ts
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) { throw new Error("请在 .env.local 中定义 MONGODB_URI"); }
interface MongooseCache { conn: typeof mongoose | null; promise: Promise<typeof mongoose> | null; }
declare global { var mongoose: MongooseCache | undefined; }
let cached: MongooseCache = global.mongoose ?? { conn: null, promise: null };
if (!global.mongoose) { global.mongoose = cached; }
export async function connectDB() {
  if (cached.conn) return cached.conn;
  if (!cached.promise) { cached.promise = mongoose.connect(MONGODB_URI!).then((m) => m); }
  cached.conn = await cached.promise;
  return cached.conn;
}
TypeScript 修正: 使用 interface 声明缓存类型,declare global 扩展全局变量类型。
// models/Comment.ts
import mongoose, { Schema, Document, Model } from "mongoose";
export interface IComment extends Document { postId: string; author: string; content: string; createdAt: Date; }
const CommentSchema = new Schema<IComment>({
  postId: { type: String, required: true }, author: { type: String, default: "匿名" },
  content: { type: String, required: true }, createdAt: { type: Date, default: Date.now },
});
const Comment: Model<IComment> = mongoose.models.Comment || mongoose.model<IComment>("Comment", CommentSchema);
export default Comment;
// app/api/comments/route.ts
import { connectDB } from "@/lib/mongodb";
import Comment from "@/models/Comment";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const postId = searchParams.get("postId");
  await connectDB();
  const comments = await Comment.find({ postId }).sort({ createdAt: -1 });
  return NextResponse.json(comments);
}
export async function POST(request: NextRequest) {
  const { postId, author, content } = await request.json();
  if (!postId || !content) { return NextResponse.json({ error: "参数错误" }, { status: 400 }); }
  await connectDB();
  const newComment = await Comment.create({ postId, author: author || "匿名", content });
  return NextResponse.json(newComment, { status: 201 });
}
✅ 验证:/api/comments?postId=test 返回 [];POST 请求可新增评论。
📁 步骤④完成后
├── app/api/comments/route.ts
├── lib/mongodb.ts
├── models/Comment.ts
└── .env.local
5🧩 核心功能:评论组件 + 暗色主题

新增: CommentForm.tsx, CommentList.tsx, CommentSection.tsx(组合器)

// components/CommentForm.tsx
"use client";
import { useState } from "react";
interface CommentFormProps { postId: string; onCommentAdded: () => void; }
export default function CommentForm({ postId, onCommentAdded }: CommentFormProps) {
  const [author, setAuthor] = useState("");
  const [content, setContent] = useState("");
  const [loading, setLoading] = useState(false);
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault(); if (!content.trim()) return;
    setLoading(true);
    const res = await fetch("/api/comments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ postId, author: author || "匿名", content }) });
    if (res.ok) { setContent(""); setAuthor(""); onCommentAdded(); }
    setLoading(false);
  };
  return (
    <form onSubmit={handleSubmit} className="space-y-4 p-6 bg-white dark:bg-gray-800 rounded-2xl border">
      <h3 className="text-lg font-semibold">💬 发表评论</h3>
      <input type="text" placeholder="昵称(可选)" value={author} onChange={(e) => setAuthor(e.target.value)} className="w-full p-3 border rounded-xl bg-gray-50 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500" />
      <textarea placeholder="评论内容..." value={content} onChange={(e) => setContent(e.target.value)} className="w-full p-3 border rounded-xl bg-gray-50 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 resize-none" rows={4} required />
      <button disabled={loading} className="bg-gradient-to-r from-purple-400 to-blue-400 text-white px-6 py-3 rounded-xl font-semibold disabled:opacity-50">{loading ? "提交中..." : "发表评论"}</button>
    </form>
  );
}
// components/CommentList.tsx
"use client";
import { useEffect, useState } from "react";
interface Comment { _id: string; postId: string; author: string; content: string; createdAt: string; }
export default function CommentList({ postId }: { postId: string }) {
  const [comments, setComments] = useState<Comment[]>([]);
  const fetchComments = async () => { const res = await fetch(`/api/comments?postId=${postId}`); setComments(await res.json()); };
  useEffect(() => { fetchComments(); }, [postId]);
  return (
    <div className="space-y-4 mt-6">
      <h3 className="text-xl font-semibold">💬 评论 ({comments.length})</h3>
      {comments.length === 0 && <p className="text-gray-400 py-8 text-center">暂无评论</p>}
      {comments.map((c) => (<div key={c._id} className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl border">
        <div className="flex items-center gap-2 mb-2"><span className="w-8 h-8 bg-gradient-to-br from-purple-400 to-blue-400 rounded-full flex items-center justify-center text-white text-sm font-bold">{c.author[0]}</span><p className="font-semibold text-sm">{c.author}</p><span className="text-xs text-gray-400 ml-auto">{new Date(c.createdAt).toLocaleDateString()}</span></div>
        <p className="text-gray-600 dark:text-gray-300 ml-10">{c.content}</p>
      </div>))}
    </div>
  );
}
// components/CommentSection.tsx(组合器)
"use client";
import { useState } from "react";
import CommentForm from "./CommentForm";
import CommentList from "./CommentList";
export default function CommentSection({ postId }: { postId: string }) {
  const [refreshKey, setRefreshKey] = useState(0);
  const refresh = () => setRefreshKey((prev) => prev + 1);
  return (
    <section className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
      <CommentForm postId={postId} onCommentAdded={refresh} />
      <div className="mt-8"><CommentList key={refreshKey} postId={postId} /></div>
    </section>
  );
}
✅ 验证:文章底部显示评论表单,提交后列表自动刷新;主题切换正常。
📁 步骤⑤完成后
├── components/ → CommentForm.tsx, CommentList.tsx, CommentSection.tsx
├── lib/mongodb.ts, models/Comment.ts
└── app/api/comments/route.ts
🚀 部署上线 · Vercel 部署指南

完成基础搭建和功能增强后,将博客部署到线上。

6🚀 部署到 Vercel 线上
准备 Git
# .gitignore
node_modules/
.next/
.env
.env.local
.DS_Store
.vercel

# 初始化 Git
git init
git add .
git commit -m "feat: 完整博客系统 - Next.js + MongoDB + 评论 + 暗色主题"
推送到 GitHub
git remote add origin https://github.com/你的用户名/my-blog.git
git branch -M main
git push -u origin main
Vercel 部署
  1. 访问 vercel.com,用 GitHub 账号登录
  2. 点击 "Add New Project" → 选择 my-blog 仓库
  3. 框架自动检测为 Next.js
  4. 关键:在 "Environment Variables" 中添加:
    MONGODB_URI = mongodb+srv://你的账号:***@cluster.mongodb.net/blog?retryWrites=true&w=majority
  5. 点击 "Deploy",等待 1-2 分钟
✅ 线上验证:访问 xxx.vercel.app,评论功能正常,主题切换持久化。
恭喜!你拥有了一个全栈博客系统。
🎓 进阶方向

博客上线后,可以继续探索以下方向:

方向内容推荐章节
🔍 SEO 优化metadata API、sitemap、JSON-LD§15 文件式路由与布局
⚡ 性能优化next/image、next/dynamic、懒加载§18 样式与资源处理
🔐 认证系统NextAuth.js、GitHub OAuth 登录
📝 内容管理MDX 本地 Markdown、Headless CMS§21 使用第三方React组件
🌍 多语言next-intl、i18n 路由
🧪 自动化测试Jest 单元测试、Playwright E2E
📊 监控分析Vercel Analytics、Sentry 错误监控§19 部署与环境变量