Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Next.js 是一个基于 React 的开源 Web 开发框架,由 Vercel 公司开发和维护。它提供了许多开箱即用的功能,使得构建现代化的 React 应用变得更加简单高效。

对于博客系统而言,Next.js 是一个理想的选择,因为它提供了优秀的 SEO 支持、快速的页面加载速度以及灵活的数据获取方式。

目录

  1. Next.js 是什么
  2. Next.js 的主要特性
  3. 环境准备
  4. 创建第一个 Next.js 应用
  5. 项目结构解析
  6. 页面和路由
  7. 样式处理
  8. 数据获取
  9. API 路由
  10. 性能优化
  11. SEO 优化
  12. 部署应用
  13. Next.js 16 新特性

1. Next.js 是什么

Next.js 是一个基于 React 的开源 Web 开发框架,由 Vercel 公司开发和维护。它提供了许多开箱即用的功能,使得构建现代化的 React 应用变得更加简单高效。

对于博客系统而言,Next.js 是一个理想的选择,因为它提供了优秀的 SEO 支持、快速的页面加载速度以及灵活的数据获取方式。


2.Next.js 的主要特性

Next.js 提供了许多强大的特性,特别适合构建博客系统:

服务器端渲染 (SSR)

1
2
3
- 提高博客文章的首屏加载速度
- 有利于 SEO,搜索引擎可以更好地抓取文章内容
- 适用于动态内容,如最新文章列表

静态站点生成 (SSG)

1
2
3
- 将博客文章预渲染成静态 HTML 文件
- 适用于内容相对固定的博客文章
- 提供极快的加载速度和更好的性能

增量静态再生 (ISR)

1
2
3
- 结合 SSR 和 SSG 的优势
- 可以定期更新博客文章,保持内容新鲜
- 平衡性能和内容实时性

API 路由

1
2
3
- 内置 API 路由功能,可以轻松创建博客系统的后台接口
- 实现文章管理、评论系统、用户认证等功能
- 无需单独维护后端服务

文件系统路由

1
2
3
- 根据文件系统结构自动生成博客路由
- 简化路由配置,易于管理
- 支持动态路由,如 /posts/[id]

图像优化

1
2
3
- 内置图像优化功能
- 自动优化博客文章中的图片大小和格式
- 提升页面加载速度和用户体验

TypeScript 支持

1
2
3
- 原生支持 TypeScript
- 提供静态类型检查,提高代码质量和可维护性
- 减少运行时错误

3.环境准备

在开始之前,确保你的开发环境满足以下要求:

Node.js 安装

Next.js 需要 Node.js 18.17 或更高版本。你可以通过以下命令检查是否已安装:

1
2
node -v
npm -v

如果没有安装 Node.js,可以从 Node.js官网 下载并安装。

代码编辑器推荐

  • Visual Studio Code (推荐)
  • WebStorm
  • Atom

包管理器

Next.js 支持多种包管理器:

  • npm (Node.js 自带)
  • yarn
  • pnpm (推荐,性能更好)

4.创建第一个 Next.js 应用

使用 create-next-app 工具快速创建一个新的 Next.js 项目:

1
2
3
4
5
6
7
8
# 使用 npx 创建项目
npx create-next-app@latest my-blog

# 或者使用 yarn
yarn create next-app my-blog

# 或者使用 pnpm
pnpm create next-app my-blog

创建过程中会提示你选择一些选项:

  • 是否使用 TypeScript
  • 是否使用 ESLint
  • 是否使用 Tailwind CSS
  • 是否使用 src/ 目录
  • 是否使用 App Router (推荐)
  • 是否自定义导入别名

对于博客项目,建议选择:

  • TypeScript: 是
  • ESLint: 是
  • Tailwind CSS: 是
  • src/ 目录: 是
  • App Router: 是
  • 自定义导入别名: 否

选择完成后,进入项目目录并启动开发服务器:

1
2
cd my-blog
npm run dev

打开浏览器访问 http://localhost:3000,你将看到 Next.js 的欢迎页面。

锁定引擎版本

为了确保团队成员使用相同的 Node.js 和包管理器版本,可以在 package.json 中添加引擎限制:

1
2
3
4
5
6
{
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
}
}

5.项目结构解析

Next.js 博客项目的基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
my-blog/
├── node_modules/
├── public/
│ ├── favicon.ico
│ ├── images/
│ │ ├── logo.png
│ │ └── posts/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── globals.css
│ │ ├── posts/
│ │ │ ├── page.tsx
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ ├── categories/
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ └── tags/
│ │ └── [id]/
│ │ └── page.tsx
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ ├── PostCard.tsx
│ │ ├── PostList.tsx
│ │ └── CategoryList.tsx
│ ├── lib/
│ │ ├── utils.ts
│ │ └── data.ts
│ └── types/
│ ├── post.ts
│ ├── category.ts
│ └── tag.ts
├── .eslintrc.json
├── .gitignore
├── next.config.js
├── package.json
├── README.md
└── tsconfig.json

博客项目关键目录说明:

  • public/: 存放静态资源文件,如网站图标、博客文章图片等
  • public/images/posts/: 存放博客文章相关的图片资源
  • src/app/: 应用的主要代码目录(使用App Router)
  • src/app/posts/: 博客文章列表页面
  • src/app/posts/[id]/: 单篇博客文章详情页面
  • src/app/categories/[id]/: 分类页面
  • src/app/tags/[id]/: 标签页面
  • src/components/: 可复用的React组件,如文章卡片、列表组件等
  • src/lib/: 工具函数和数据处理逻辑
  • src/types/: TypeScript 类型定义文件
  • next.config.js: Next.js 配置文件
  • package.json: 项目依赖和脚本配置

6.页面和路由

Next.js 使用基于文件系统的路由机制,这对于博客系统非常友好。在 src/app/ 目录下创建文件即可自动生成对应路由。

博客首页创建

创建博客系统的首页,在 src/app/ 目录下新建 page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// src/app/page.tsx
import Link from 'next/link';

interface Post {
id: string;
title: string;
excerpt: string;
date: string;
category: string;
}

export default function HomePage() {
// 模拟博客文章数据
const posts: Post[] = [
{
id: '1',
title: 'Next.js 16 新特性介绍',
excerpt: '本文介绍了 Next.js 16 版本的最新特性和改进...',
date: '2025-01-15',
category: '技术'
},
{
id: '2',
title: 'React Server Components 深入解析',
excerpt: '深入了解 React Server Components 的工作原理和应用场景...',
date: '2025-01-10',
category: '技术'
}
];

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<header className="text-center mb-16">
<h1 className="text-4xl font-bold text-gray-900 mb-4">我的技术博客</h1>
<p className="text-xl text-gray-600">分享前端技术和开发经验</p>
</header>

<section className="mb-16">
<h2 className="text-2xl font-bold text-gray-900 mb-6">最新文章</h2>
<div className="grid gap-8 md:grid-cols-2">
{posts.map(post => (
<article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="p-6">
<div className="flex justify-between items-center mb-3">
<span className="inline-block px-3 py-1 text-xs font-semibold text-blue-600 bg-blue-100 rounded-full">
{post.category}
</span>
<time className="text-sm text-gray-500">{post.date}</time>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
<Link href={`/posts/${post.id}`} className="hover:text-blue-600 transition-colors">
{post.title}
</Link>
</h3>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<Link
href={`/posts/${post.id}`}
className="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
阅读全文
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
</svg>
</Link>
</div>
</article>
))}
</div>
</section>

<section>
<h2 className="text-2xl font-bold text-gray-900 mb-6">关于本站</h2>
<div className="bg-white rounded-lg shadow-md p-6">
<p className="text-gray-600 mb-4">这是一个使用 Next.js 构建的技术博客,专注于分享前端开发技术、React、Next.js 等框架的使用经验和最佳实践。</p>
<p className="text-gray-600">在这里,你可以找到最新的技术文章、教程和开发心得。</p>
</div>
</section>
</div>
</div>
);
}

访问 http://localhost:3000 即可看到博客首页。

路由跳转

Next.js 提供了多种路由跳转方式,非常适合博客系统的导航需求:

在博客系统中,我们经常需要在文章列表和详情页之间跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/components/PostCard.tsx
import Link from 'next/link';

interface Post {
id: string;
title: string;
excerpt: string;
date: string;
category: string;
}

export default function PostCard({ post }: { post: Post }) {
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="p-6">
<div className="flex justify-between items-center mb-3">
<span className="inline-block px-3 py-1 text-xs font-semibold text-blue-600 bg-blue-100 rounded-full">
{post.category}
</span>
<time className="text-sm text-gray-500">{post.date}</time>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
<Link href={`/posts/${post.id}`} className="hover:text-blue-600 transition-colors">
{post.title}
</Link>
</h3>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<Link
href={`/posts/${post.id}`}
className="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
阅读全文
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
</svg>
</Link>
</div>
</article>
);
}

使用 useRouter 钩子

在博客系统的搜索功能中,我们可以使用 useRouter 钩子来处理复杂的路由跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/components/SearchForm.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useState } from 'react';

export default function SearchForm() {
const router = useRouter();
const [query, setQuery] = useState('');

const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
// 跳转到搜索结果页面
router.push(`/search?q=${encodeURIComponent(query)}`);
}
};

return (
<form onSubmit={handleSearch} className="mb-8">
<div className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索文章..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
搜索
</button>
</div>
</form>
);
}

动态路由

博客系统中最常用的动态路由是文章详情页,在 src/app/ 目录下新建 posts/[id]/page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// src/app/posts/[id]/page.tsx
export default function PostDetail({ params }: { params: { id: string } }) {
// 这里应该从数据库或 API 获取文章数据
const post = {
id: params.id,
title: `深入理解 Next.js 16 的新特性`,
content: `
<p>Next.js 16 带来了许多令人兴奋的新特性,包括 React Server Components、Partial Prerendering 等。</p>

<h2>React Server Components</h2>
<p>React Server Components 允许我们在服务器上渲染组件,从而减少客户端 JavaScript 的大小,提高页面加载速度。</p>

<h2>Partial Prerendering</h2>
<p>Partial Prerendering 结合了静态生成和服务器端渲染的优势,为我们提供了更好的性能和灵活性。</p>

<p>这些新特性使得 Next.js 成为构建现代 Web 应用的强大工具。</p>
`,
author: '张三',
date: '2025-01-15',
category: '技术',
tags: ['Next.js', 'React', '前端']
};

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<article className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="p-8">
<div className="flex flex-wrap items-center gap-4 mb-6 text-sm text-gray-600">
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full">
{post.category}
</span>
<span>作者: {post.author}</span>
<span>发布日期: {post.date}</span>
</div>

<h1 className="text-3xl font-bold text-gray-900 mb-6">{post.title}</h1>

<div
className="prose max-w-none prose-lg"
dangerouslySetInnerHTML={{ __html: post.content }}
/>

<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex flex-wrap gap-2">
{post.tags.map(tag => (
<span
key={tag}
className="inline-block px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full"
>
#{tag}
</span>
))}
</div>
</div>
</div>
</article>

<div className="mt-8 flex justify-between">
<button
onClick={() => window.history.back()}
className="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
← 返回
</button>
<button className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
分享文章
</button>
</div>
</div>
</div>
);
}

访问 http://localhost:3000/posts/1 可以看到文章详情页。

嵌套路由

在博客系统中,我们可以创建分类和标签的嵌套路由。例如,创建分类页面,在 src/app/categories/ 目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/app/categories/layout.tsx
export default function CategoriesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<header className="mb-8 text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2">文章分类</h1>
<p className="text-gray-600">浏览不同分类下的文章</p>
</header>

<div className="flex flex-col md:flex-row gap-8">
<aside className="md:w-1/4">
<div className="bg-white rounded-lg shadow-md p-6 sticky top-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">所有分类</h2>
<nav>
<ul className="space-y-2">
<li>
<a href="/categories/technology" className="text-blue-600 hover:underline">技术</a>
</li>
<li>
<a href="/categories/tutorial" className="text-blue-600 hover:underline">教程</a>
</li>
<li>
<a href="/categories/news" className="text-blue-600 hover:underline">新闻</a>
</li>
<li>
<a href="/categories/opinion" className="text-blue-600 hover:underline">观点</a>
</li>
</ul>
</nav>
</div>
</aside>

<main className="md:w-3/4">
{children}
</main>
</div>
</div>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/app/categories/page.tsx
export default function CategoriesOverview() {
const categories = [
{ id: 'technology', name: '技术', count: 25, description: '前端技术、框架使用等' },
{ id: 'tutorial', name: '教程', count: 18, description: '详细的步骤指南和教程' },
{ id: 'news', name: '新闻', count: 12, description: '行业动态和技术新闻' },
{ id: 'opinion', name: '观点', count: 8, description: '个人观点和思考' },
];

return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-6">分类概览</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{categories.map(category => (
<div key={category.id} className="border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-medium text-gray-900">
<a href={`/categories/${category.id}`} className="hover:text-blue-600">
{category.name}
</a>
</h3>
<span className="inline-block px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded-full">
{category.count} 篇文章
</span>
</div>
<p className="text-gray-600">{category.description}</p>
</div>
))}
</div>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/app/categories/[id]/page.tsx
export default function CategoryPosts({ params }: { params: { id: string } }) {
// 根据分类ID获取文章
const categoryMap: Record<string, { name: string; description: string }> = {
technology: { name: '技术', description: '前端技术、框架使用等' },
tutorial: { name: '教程', description: '详细的步骤指南和教程' },
news: { name: '新闻', description: '行业动态和技术新闻' },
opinion: { name: '观点', description: '个人观点和思考' },
};

const category = categoryMap[params.id] || { name: '未知分类', description: '' };

// 模拟该分类下的文章
const posts = [
{ id: '1', title: 'Next.js 16 新特性详解', excerpt: '深入了解 Next.js 16 的最新功能...', date: '2025-01-15' },
{ id: '2', title: 'React Server Components 实践', excerpt: '如何在项目中使用 React Server Components...', date: '2025-01-10' },
{ id: '3', title: 'TypeScript 高级技巧', excerpt: '提升 TypeScript 编码效率的技巧...', date: '2025-01-05' },
];

return (
<div>
<div className="mb-8 p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold text-gray-900 mb-2">{category.name}</h2>
<p className="text-gray-600">{category.description}</p>
</div>

<div className="space-y-6">
{posts.map(post => (
<article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<div className="p-6">
<h3 className="text-xl font-bold text-gray-900 mb-3">
<a href={`/posts/${post.id}`} className="hover:text-blue-600">
{post.title}
</a>
</h3>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex justify-between items-center">
<time className="text-sm text-gray-500">{post.date}</time>
<a
href={`/posts/${post.id}`}
className="text-blue-600 hover:text-blue-800 font-medium"
>
阅读全文
</a>
</div>
</div>
</article>
))}
</div>
</div>
);
}

7.样式处理

Next.js 支持多种样式方案:

全局样式

src/app/globals.css 中添加全局样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
}

.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}

在根布局中引入全局样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: '我的博客系统',
description: '使用 Next.js 构建的现代博客系统',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>{children}</body>
</html>
);
}

CSS Modules

创建组件级样式文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* src/components/Button.module.css */
.button {
background-color: #0070f3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}

.button:hover {
background-color: #0051a2;
}

在组件中使用:

1
2
3
4
5
6
7
8
9
10
// src/components/Button.tsx
import styles from './Button.module.css';

export default function Button({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
return (
<button className={styles.button} onClick={onClick}>
{children}
</button>
);
}

Tailwind CSS

如果在创建项目时选择了 Tailwind CSS,可以直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function HomePage() {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full">
<h1 className="text-3xl font-bold text-gray-800 mb-4 text-center">欢迎来到博客系统</h1>
<p className="text-gray-600 text-center mb-6">这是一个使用 Next.js 和 Tailwind CSS 构建的现代化博客系统</p>
<div className="flex justify-center gap-4">
<button className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition duration-300">
开始阅读
</button>
<button className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition duration-300">
了解更多
</button>
</div>
</div>
</div>
);
}

8.数据获取

在博客系统中,Next.js 提供了多种数据获取方式来展示文章内容:

服务端获取博客文章数据 (SSR)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// src/app/posts/page.tsx
interface Post {
id: string;
title: string;
excerpt: string;
content: string;
date: string;
author: string;
category: string;
tags: string[];
}

async function getPosts() {
// 在实际博客应用中,这里应该调用 API 或数据库
// 模拟获取博客文章数据
const posts: Post[] = [
{
id: '1',
title: 'Next.js 16 新特性详解',
excerpt: '深入了解 Next.js 16 的最新功能和改进,包括 React Server Components 和 Partial Prerendering...',
content: '<p>Next.js 16 带来了许多令人兴奋的新特性...</p>',
date: '2025-01-15',
author: '张三',
category: '技术',
tags: ['Next.js', 'React', '前端']
},
{
id: '2',
title: 'React Server Components 实践指南',
excerpt: '详细介绍如何在实际项目中使用 React Server Components 来提升性能...',
content: '<p>React Server Components 是 React 的一项重要新特性...</p>',
date: '2025-01-10',
author: '李四',
category: '技术',
tags: ['React', '前端', '性能优化']
}
];

return posts;
}

export default async function PostsPage() {
const posts = await getPosts();

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<header className="mb-8 text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-2">所有文章</h1>
<p className="text-gray-600">发现最新的技术文章和教程</p>
</header>

<div className="grid gap-8 md:grid-cols-2">
{posts.map(post => (
<article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
<div className="p-6">
<div className="flex justify-between items-center mb-3">
<span className="inline-block px-3 py-1 text-xs font-semibold text-blue-600 bg-blue-100 rounded-full">
{post.category}
</span>
<time className="text-sm text-gray-500">{post.date}</time>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-3">
<a href={`/posts/${post.id}`} className="hover:text-blue-600 transition-colors">
{post.title}
</a>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex flex-wrap gap-2 mb-4">
{post.tags.map(tag => (
<span key={tag} className="text-xs text-gray-500">#{tag}</span>
))}
</div>
<a
href={`/posts/${post.id}`}
className="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium"
>
阅读全文
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</article>
))}
</div>
</div>
</div>
);
}

客户端获取实时数据

在博客系统中,某些实时数据(如评论数、点赞数)可以通过客户端获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/components/PostStats.tsx
'use client';

import { useEffect, useState } from 'react';

interface PostStats {
views: number;
likes: number;
comments: number;
}

export default function PostStats({ postId }: { postId: string }) {
const [stats, setStats] = useState<PostStats | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 获取文章统计数据
fetch(`/api/posts/${postId}/stats`)
.then(res => res.json())
.then(data => {
setStats(data.stats);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, [postId]);

if (loading) {
return <div className="flex space-x-4 text-sm text-gray-500">
<span>加载中...</span>
</div>;
}

if (!stats) {
return null;
}

return (
<div className="flex space-x-4 text-sm text-gray-500">
<span>👁️ {stats.views} 阅读</span>
<span>👍 {stats.likes} 点赞</span>
<span>💬 {stats.comments} 评论</span>
</div>
);
}

静态生成博客文章 (SSG)

对于内容相对固定的博客文章,可以使用静态生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// src/app/posts/[id]/page.tsx
interface Post {
id: string;
title: string;
content: string;
date: string;
author: string;
category: string;
tags: string[];
}

async function getPost(id: string) {
// 在实际应用中,这里应该调用 API 或数据库
// 模拟获取单篇博客文章
const posts: Record<string, Post> = {
'1': {
id: '1',
title: 'Next.js 16 新特性详解',
content: `
<p>Next.js 16 带来了许多令人兴奋的新特性,包括 React Server Components、Partial Prerendering 等。</p>

<h2>React Server Components</h2>
<p>React Server Components 允许我们在服务器上渲染组件,从而减少客户端 JavaScript 的大小,提高页面加载速度。</p>

<h2>Partial Prerendering</h2>
<p>Partial Prerendering 结合了静态生成和服务器端渲染的优势,为我们提供了更好的性能和灵活性。</p>
`,
date: '2025-01-15',
author: '张三',
category: '技术',
tags: ['Next.js', 'React', '前端']
}
};

return posts[id] || null;
}

export default async function PostDetail({ params }: { params: { id: string } }) {
const post = await getPost(params.id);

if (!post) {
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4 text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">文章未找到</h1>
<p className="text-gray-600 mb-6">抱歉,您访问的文章不存在或已被删除。</p>
<a
href="/"
className="inline-block px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
返回首页
</a>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4">
<article className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="p-8">
<div className="flex flex-wrap items-center gap-4 mb-6 text-sm text-gray-600">
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full">
{post.category}
</span>
<span>作者: {post.author}</span>
<span>发布日期: {post.date}</span>
</div>

<h1 className="text-3xl font-bold text-gray-900 mb-6">{post.title}</h1>

<div
className="prose max-w-none prose-lg"
dangerouslySetInnerHTML={{ __html: post.content }}
/>

<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex flex-wrap gap-2">
{post.tags.map(tag => (
<span
key={tag}
className="inline-block px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full"
>
#{tag}
</span>
))}
</div>
</div>
</div>
</article>

<div className="mt-8 flex justify-between">
<button
onClick={() => window.history.back()}
className="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
← 返回
</button>
<button className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
分享文章
</button>
</div>
</div>
</div>
);
}

// 预渲染静态参数
export async function generateStaticParams() {
// 在实际应用中,这里应该从数据库获取所有文章ID
const postIds = ['1', '2', '3']; // 示例文章ID

return postIds.map(id => ({
id,
}));
}

缓存控制

在博客系统中,合理使用缓存可以提升性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/lib/blog-data.ts
export async function getBlogPosts(revalidate = 3600) {
const res = await fetch(`${process.env.API_URL}/posts`, {
// 对于博客文章列表,可以设置1小时缓存
next: { revalidate }
});

if (!res.ok) {
throw new Error('获取文章列表失败');
}

return res.json();
}

export async function getBlogPost(id: string) {
const res = await fetch(`${process.env.API_URL}/posts/${id}`, {
// 对于单篇文章,可以设置更长的缓存时间
next: { revalidate: 86400 } // 24小时
});

if (!res.ok) {
throw new Error('获取文章失败');
}

return res.json();
}

export async function getLatestPosts() {
const res = await fetch(`${process.env.API_URL}/posts/latest`, {
// 对于最新文章,可以设置较短的缓存时间
next: { revalidate: 300 } // 5分钟
});

if (!res.ok) {
throw new Error('获取最新文章失败');
}

return res.json();
}

9.API 路由

Next.js 不仅是一个前端框架,它还提供了强大的后端能力。通过 API 路由功能,你可以在同一个项目中创建后端 API 接口,无需单独维护后端服务。

对于博客系统而言,API 路由可以用来处理文章的增删改查、用户认证、评论管理、分类标签等核心功能。

博客系统中 API 路由的优势:

1
2
3
4
5
6
- 统一开发体验: 前后端代码在同一项目中管理,便于维护
- 简化部署: 前后端一起部署,减少运维复杂度
- 快速原型开发: 快速构建全栈博客应用
- 无缝集成: 前端可以直接调用同源 API,无跨域问题
- 自动 HTTPS: 部署到 Vercel 等平台时自动支持 HTTPS
- 内置优化: 自动代码分割、缓存控制等

博客系统 API 设计

在构建博客系统时,我们需要设计一套合理的 API 接口。以下是我们博客系统的核心 API 设计:

核心资源

  1. 文章 (Posts)

    • 获取文章列表: GET /api/posts
    • 获取单篇文章: GET /api/posts/{id}
    • 创建文章: POST /api/posts
    • 更新文章: PUT /api/posts/{id}
    • 删除文章: DELETE /api/posts/{id}
    • 搜索文章: GET /api/posts/search?q=关键词
  2. 分类 (Categories)

    • 获取分类列表: GET /api/categories
    • 获取单个分类: GET /api/categories/{id}
    • 获取分类下的文章: GET /api/categories/{id}/posts
  3. 标签 (Tags)

    • 获取标签列表: GET /api/tags
    • 获取单个标签: GET /api/tags/{id}
    • 获取标签下的文章: GET /api/tags/{id}/posts
  4. 评论 (Comments)

    • 获取文章评论: GET /api/posts/{id}/comments
    • 添加评论: POST /api/posts/{id}/comments
    • 回复评论: POST /api/comments/{id}/replies
  5. 用户 (Users)

    • 用户注册: POST /api/auth/register
    • 用户登录: POST /api/auth/login
    • 获取用户信息: GET /api/users/{id}
    • 更新用户信息: PUT /api/users/{id}

API 响应格式

所有 API 响应都遵循统一的格式,便于前端处理:

1
2
3
4
5
6
{
"success": true,
"data": {},
"message": "操作成功",
"errorCode": null
}

错误响应格式:

1
2
3
4
5
6
{
"success": false,
"data": null,
"message": "错误信息",
"errorCode": "ERROR_CODE"
}

创建基本 API 路由

在 Next.js 中,API 路由文件放置在 src/app/api/ 目录下。每个路由文件都会自动成为对应的 API 端点。

创建博客系统健康检查 API

创建 src/app/api/health/route.ts,用于检查博客系统 API 的健康状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
// 返回博客系统健康检查信息
const healthInfo = {
status: 'OK',
service: 'Blog System API',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: '1.0.0'
};

return NextResponse.json(healthInfo, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}

访问 http://localhost:3000/api/health 就可以看到返回的健康检查数据。

创建博客系统配置信息 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app/api/config/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
// 返回博客系统配置信息
const configInfo = {
blogName: process.env.BLOG_NAME || '我的技术博客',
blogDescription: process.env.BLOG_DESCRIPTION || '分享前端技术和开发经验',
version: '1.0.0',
features: [
'文章管理',
'分类标签',
'评论系统',
'用户认证',
'SEO优化'
]
};

return NextResponse.json({
success: true,
data: configInfo,
message: '获取配置信息成功'
});
}

创建博客统计数据 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app/api/stats/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
// 模拟博客统计数据(实际应用中应从数据库获取)
const stats = {
totalPosts: 128,
totalCategories: 12,
totalTags: 45,
totalComments: 356,
totalViews: 12500,
latestPost: {
id: '129',
title: 'Next.js 16 新特性详解',
date: '2025-01-15'
}
};

return NextResponse.json({
success: true,
data: stats,
message: '获取统计数据成功'
});
}

处理不同 HTTP 方法

API 路由可以处理各种 HTTP 方法,如 GET、POST、PUT、DELETE 等。在博客系统中,我们需要为文章资源实现完整的 CRUD 操作。

博客文章资源的完整 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// src/app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { Post } from '@/types/post';

// 模拟数据库中的文章数据
let posts: Post[] = [
{
id: '1',
title: 'Next.js 16 新特性详解',
content: '<p>Next.js 16 带来了许多令人兴奋的新特性,包括 React Server Components、Partial Prerendering 等。</p><p>这些新特性使得 Next.js 成为构建现代 Web 应用的强大工具。</p>',
excerpt: '深入了解 Next.js 16 的最新功能和改进,包括 React Server Components 和 Partial Prerendering...',
slug: 'nextjs-16-features',
categoryId: '1',
tags: ['Next.js', 'React', '前端'],
author: '张三',
authorId: '1',
createdAt: '2025-01-15T10:00:00Z',
updatedAt: '2025-01-15T10:00:00Z',
published: true,
views: 1250,
likes: 42
},
{
id: '2',
title: 'React Server Components 实践指南',
content: '<p>React Server Components 是 React 的一项重要新特性,它允许我们在服务器上渲染组件,从而减少客户端 JavaScript 的大小,提高页面加载速度。</p>',
excerpt: '详细介绍如何在实际项目中使用 React Server Components 来提升性能...',
slug: 'react-server-components-guide',
categoryId: '1',
tags: ['React', '前端', '性能优化'],
author: '李四',
authorId: '2',
createdAt: '2025-01-10T14:30:00Z',
updatedAt: '2025-01-10T14:30:00Z',
published: true,
views: 980,
likes: 36
}
];

// 处理 GET 请求 - 获取文章列表
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const categoryId = searchParams.get('categoryId') || '';
const tag = searchParams.get('tag') || '';
const search = searchParams.get('search') || '';

// 过滤逻辑
let filteredPosts = posts.filter(post => post.published);

// 按分类过滤
if (categoryId) {
filteredPosts = filteredPosts.filter(post => post.categoryId === categoryId);
}

// 按标签过滤
if (tag) {
filteredPosts = filteredPosts.filter(post => post.tags.includes(tag));
}

// 搜索过滤
if (search) {
const searchLower = search.toLowerCase();
filteredPosts = filteredPosts.filter(post =>
post.title.toLowerCase().includes(searchLower) ||
post.content.toLowerCase().includes(searchLower) ||
post.excerpt.toLowerCase().includes(searchLower)
);
}

// 分页逻辑
const startIndex = (page - 1) * limit;
const paginatedPosts = filteredPosts.slice(startIndex, startIndex + limit);

return NextResponse.json({
success: true,
data: {
posts: paginatedPosts.map(post => ({
id: post.id,
title: post.title,
excerpt: post.excerpt,
slug: post.slug,
categoryId: post.categoryId,
tags: post.tags,
author: post.author,
createdAt: post.createdAt,
views: post.views,
likes: post.likes
})),
pagination: {
currentPage: page,
totalPages: Math.ceil(filteredPosts.length / limit),
totalItems: filteredPosts.length,
itemsPerPage: limit
}
},
message: '获取文章列表成功'
});
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '获取文章列表失败',
errorCode: 'FETCH_POSTS_FAILED'
}, { status: 500 });
}
}

// 处理 POST 请求 - 创建新文章
export async function POST(request: Request) {
try {
// 在实际应用中,这里应该验证用户身份
// const userId = await verifyAuthToken(request);

const body = await request.json();

// 验证必需字段
if (!body.title || !body.content) {
return NextResponse.json({
success: false,
data: null,
message: '标题和内容是必需的',
errorCode: 'MISSING_REQUIRED_FIELDS'
}, { status: 400 });
}

// 创建新文章
const newPost: Post = {
id: Date.now().toString(),
title: body.title,
content: body.content,
excerpt: body.excerpt || body.content.substring(0, 150),
slug: body.slug || encodeURIComponent(body.title.toLowerCase().replace(/\s+/g, '-')),
categoryId: body.categoryId || '1',
tags: body.tags || [],
author: body.author || '匿名作者',
authorId: body.authorId || '1',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
published: body.published !== undefined ? body.published : false,
views: 0,
likes: 0
};

posts.push(newPost);

return NextResponse.json({
success: true,
data: {
id: newPost.id,
title: newPost.title,
slug: newPost.slug,
excerpt: newPost.excerpt,
categoryId: newPost.categoryId,
tags: newPost.tags,
author: newPost.author,
createdAt: newPost.createdAt,
published: newPost.published
},
message: '文章创建成功'
}, { status: 201 });
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '创建文章失败',
errorCode: 'CREATE_POST_FAILED'
}, { status: 500 });
}
}

测试博客 API 路由

可以使用 curl 命令或 Postman 测试博客 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# GET 请求 - 获取文章列表
curl http://localhost:3000/api/posts

curl "http://localhost:3000/api/posts?page=1&limit=5&categoryId=1"

curl "http://localhost:3000/api/posts?search=Next.js"

# POST 请求 - 创建新文章
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{
"title": "新文章标题",
"content": "<p>文章内容...</p><p>支持 HTML 格式</p>",
"excerpt": "文章摘要",
"categoryId": "1",
"tags": ["技术", "Next.js"],
"author": "作者姓名",
"published": true
}'

动态 API 路由

Next.js 支持动态 API 路由,这对于处理单个资源非常有用。在博客系统中,我们需要通过文章 ID 来获取、更新或删除特定文章,也需要处理文章的评论、点赞等交互功能。

单篇文章 API

创建 src/app/api/posts/[id]/route.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// src/app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';
import { Post } from '@/types/post';

// 模拟数据库中的文章数据
let posts: Post[] = [
{
id: '1',
title: 'Next.js 16 新特性详解',
content: '<p>Next.js 16 带来了许多令人兴奋的新特性,包括 React Server Components、Partial Prerendering 等。</p><p>这些新特性使得 Next.js 成为构建现代 Web 应用的强大工具。</p>',
excerpt: '深入了解 Next.js 16 的最新功能和改进,包括 React Server Components 和 Partial Prerendering...',
slug: 'nextjs-16-features',
categoryId: '1',
tags: ['Next.js', 'React', '前端'],
author: '张三',
authorId: '1',
createdAt: '2025-01-15T10:00:00Z',
updatedAt: '2025-01-15T10:00:00Z',
published: true,
views: 1250,
likes: 42
}
];

// 处理 GET 请求 - 获取单篇文章
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;

// 查找文章
const post = posts.find(p => p.id === id && p.published);

if (!post) {
return NextResponse.json({
success: false,
data: null,
message: '文章未找到或未发布',
errorCode: 'POST_NOT_FOUND'
}, { status: 404 });
}

// 增加浏览量
post.views += 1;

return NextResponse.json({
success: true,
data: post,
message: '获取文章成功'
});
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '获取文章失败',
errorCode: 'FETCH_POST_FAILED'
}, { status: 500 });
}
}

// 处理 PUT 请求 - 更新文章
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// 在实际应用中,这里应该验证用户身份和权限
// const userId = await verifyAuthToken(request);
// const userRole = await getUserRole(userId);
// if (userRole !== 'admin' && userRole !== 'author') {
// return NextResponse.json({
// success: false,
// data: null,
// message: '无权限更新文章',
// errorCode: 'UNAUTHORIZED'
// }, { status: 403 });
// }

const { id } = params;
const body = await request.json();

const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) {
return NextResponse.json({
success: false,
data: null,
message: '文章未找到',
errorCode: 'POST_NOT_FOUND'
}, { status: 404 });
}

// 更新文章
const updatedPost = {
...posts[postIndex],
...body,
id: posts[postIndex].id, // 保持原有 ID
updatedAt: new Date().toISOString()
};

posts[postIndex] = updatedPost;

return NextResponse.json({
success: true,
data: {
id: updatedPost.id,
title: updatedPost.title,
slug: updatedPost.slug,
excerpt: updatedPost.excerpt,
categoryId: updatedPost.categoryId,
tags: updatedPost.tags,
author: updatedPost.author,
updatedAt: updatedPost.updatedAt,
published: updatedPost.published
},
message: '文章更新成功'
});
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '更新文章失败',
errorCode: 'UPDATE_POST_FAILED'
}, { status: 500 });
}
}

// 处理 DELETE 请求 - 删除文章
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// 在实际应用中,这里应该验证用户身份和权限
// const userId = await verifyAuthToken(request);
// const userRole = await getUserRole(userId);
// if (userRole !== 'admin' && userRole !== 'author') {
// return NextResponse.json({
// success: false,
// data: null,
// message: '无权限删除文章',
// errorCode: 'UNAUTHORIZED'
// }, { status: 403 });
// }

const { id } = params;

const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) {
return NextResponse.json({
success: false,
data: null,
message: '文章未找到',
errorCode: 'POST_NOT_FOUND'
}, { status: 404 });
}

// 删除文章
const deletedPost = posts.splice(postIndex, 1)[0];

return NextResponse.json({
success: true,
data: {
id: deletedPost.id,
title: deletedPost.title
},
message: '文章删除成功'
});
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '删除文章失败',
errorCode: 'DELETE_POST_FAILED'
}, { status: 500 });
}
}

// 处理 PATCH 请求 - 文章交互操作(点赞等)
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const body = await request.json();

const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) {
return NextResponse.json({
success: false,
data: null,
message: '文章未找到',
errorCode: 'POST_NOT_FOUND'
}, { status: 404 });
}

// 处理点赞
if (body.action === 'like') {
posts[postIndex].likes += 1;
return NextResponse.json({
success: true,
data: { likes: posts[postIndex].likes },
message: '点赞成功'
});
}

// 处理取消点赞
if (body.action === 'unlike') {
posts[postIndex].likes = Math.max(0, posts[postIndex].likes - 1);
return NextResponse.json({
success: true,
data: { likes: posts[postIndex].likes },
message: '取消点赞成功'
});
}

return NextResponse.json({
success: false,
data: null,
message: '不支持的操作',
errorCode: 'UNSUPPORTED_ACTION'
}, { status: 400 });
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '操作失败',
errorCode: 'POST_ACTION_FAILED'
}, { status: 500 });
}
}

嵌套资源路由 - 文章评论

对于评论等嵌套资源,我们可以创建嵌套路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// src/app/api/posts/[postId]/comments/route.ts
import { NextResponse } from 'next/server';
import { Comment } from '@/types/comment';

// 模拟评论数据
let comments: Comment[] = [
{
id: '1',
postId: '1',
author: '读者甲',
authorEmail: 'reader@example.com',
content: '这篇文章写得很好,学到了很多新知识!',
createdAt: '2025-01-16T09:30:00Z',
likes: 5
},
{
id: '2',
postId: '1',
author: '读者乙',
authorEmail: 'reader2@example.com',
content: '期待更多关于 React Server Components 的内容。',
createdAt: '2025-01-16T14:20:00Z',
likes: 3
}
];

// 处理 GET 请求 - 获取文章评论
export async function GET(
request: Request,
{ params }: { params: { postId: string } }
) {
try {
const { postId } = params;

// 验证文章是否存在
// const post = await getPostById(postId);
// if (!post) {
// return NextResponse.json({
// success: false,
// data: null,
// message: '文章未找到',
// errorCode: 'POST_NOT_FOUND'
// }, { status: 404 });
// }

// 获取指定文章的评论
const postComments = comments
.filter(comment => comment.postId === postId)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

return NextResponse.json({
success: true,
data: postComments,
message: '获取评论成功'
});
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '获取评论失败',
errorCode: 'FETCH_COMMENTS_FAILED'
}, { status: 500 });
}
}

// 处理 POST 请求 - 添加评论
export async function POST(
request: Request,
{ params }: { params: { postId: string } }
) {
try {
const { postId } = params;
const body = await request.json();

// 验证必需字段
if (!body.author || !body.content) {
return NextResponse.json({
success: false,
data: null,
message: '作者和内容是必需的',
errorCode: 'MISSING_REQUIRED_FIELDS'
}, { status: 400 });
}

// 验证文章是否存在
// const post = await getPostById(postId);
// if (!post) {
// return NextResponse.json({
// success: false,
// data: null,
// message: '文章未找到',
// errorCode: 'POST_NOT_FOUND'
// }, { status: 404 });
// }

// 创建新评论
const newComment: Comment = {
id: Date.now().toString(),
postId: postId,
author: body.author,
authorEmail: body.authorEmail || '',
content: body.content,
createdAt: new Date().toISOString(),
likes: 0
};

comments.push(newComment);

return NextResponse.json({
success: true,
data: {
id: newComment.id,
author: newComment.author,
content: newComment.content,
createdAt: newComment.createdAt
},
message: '评论添加成功'
}, { status: 201 });
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '添加评论失败',
errorCode: 'ADD_COMMENT_FAILED'
}, { status: 500 });
}
}

// 处理 PATCH 请求 - 评论点赞
export async function PATCH(
request: Request,
{ params }: { params: { postId: string } }
) {
try {
const { postId } = params;
const body = await request.json();

const commentIndex = comments.findIndex(c => c.id === body.commentId && c.postId === postId);
if (commentIndex === -1) {
return NextResponse.json({
success: false,
data: null,
message: '评论未找到',
errorCode: 'COMMENT_NOT_FOUND'
}, { status: 404 });
}

// 处理点赞
if (body.action === 'like') {
comments[commentIndex].likes += 1;
return NextResponse.json({
success: true,
data: { likes: comments[commentIndex].likes },
message: '点赞成功'
});
}

// 处理取消点赞
if (body.action === 'unlike') {
comments[commentIndex].likes = Math.max(0, comments[commentIndex].likes - 1);
return NextResponse.json({
success: true,
data: { likes: comments[commentIndex].likes },
message: '取消点赞成功'
});
}

return NextResponse.json({
success: false,
data: null,
message: '不支持的操作',
errorCode: 'UNSUPPORTED_ACTION'
}, { status: 400 });
} catch (error) {
return NextResponse.json({
success: false,
data: null,
message: '操作失败',
errorCode: 'COMMENT_ACTION_FAILED'
}, { status: 500 });
}
}

数据库集成

在实际的博客系统中,我们需要将数据存储在真实的数据库中。这里以 MongoDB 和 Mongoose 为例,展示如何集成数据库:

安装依赖

1
npm install mongoose

数据库连接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/lib/database.ts
import mongoose from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {
throw new Error('请设置 MONGODB_URI 环境变量');
}

interface GlobalWithMongoose extends Global {
mongoose: any;
}

declare const global: GlobalWithMongoose;

let cached = global.mongoose;

if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}

export async function connectToDatabase() {
if (cached.conn) {
return cached.conn;
}

if (!cached.promise) {
const opts = {
bufferCommands: false,
};

cached.promise = mongoose.connect(MONGODB_URI!, opts).then((mongoose) => {
return mongoose;
});
}

try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}

return cached.conn;
}

博客系统数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// src/models/Post.ts
import mongoose, { Schema, Document } from 'mongoose';

export interface IPost extends Document {
title: string;
content: string;
excerpt: string;
slug: string;
categoryId: string;
tags: string[];
author: string;
authorId: string;
createdAt: Date;
updatedAt: Date;
published: boolean;
views: number;
likes: number;
}

const PostSchema: Schema = new Schema({
title: { type: String, required: true, trim: true },
content: { type: String, required: true }, // 支持 HTML 内容
excerpt: { type: String, trim: true },
slug: { type: String, required: true, unique: true, lowercase: true },
categoryId: { type: String, required: true, ref: 'Category' },
tags: [{ type: String, trim: true }],
author: { type: String, required: true, trim: true },
authorId: { type: String, required: true, ref: 'User' },
published: { type: Boolean, default: false },
views: { type: Number, default: 0 },
likes: { type: Number, default: 0 }
}, {
timestamps: true
});

// 添加索引
PostSchema.index({ title: 'text', content: 'text', excerpt: 'text' });
PostSchema.index({ categoryId: 1 });
PostSchema.index({ tags: 1 });
PostSchema.index({ authorId: 1 });
PostSchema.index({ published: 1 });
PostSchema.index({ createdAt: -1 });

export default mongoose.models.Post || mongoose.model<IPost>('Post', PostSchema);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/models/Category.ts
import mongoose, { Schema, Document } from 'mongoose';

export interface ICategory extends Document {
name: string;
slug: string;
description: string;
createdAt: Date;
updatedAt: Date;
}

const CategorySchema: Schema = new Schema({
name: { type: String, required: true, unique: true, trim: true },
slug: { type: String, required: true, unique: true, lowercase: true },
description: { type: String, trim: true }
}, {
timestamps: true
});

export default mongoose.models.Category || mongoose.model<ICategory>('Category', CategorySchema);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/models/Comment.ts
import mongoose, { Schema, Document } from 'mongoose';

export interface IComment extends Document {
postId: string;
author: string;
authorEmail: string;
content: string;
parentId?: string; // 用于回复评论
likes: number;
createdAt: Date;
updatedAt: Date;
}

const CommentSchema: Schema = new Schema({
postId: { type: String, required: true, ref: 'Post' },
author: { type: String, required: true, trim: true },
authorEmail: { type: String, trim: true },
content: { type: String, required: true },
parentId: { type: String, ref: 'Comment' }, // 用于回复评论
likes: { type: Number, default: 0 }
}, {
timestamps: true
});

CommentSchema.index({ postId: 1 });
CommentSchema.index({ parentId: 1 });

export default mongoose.models.Comment || mongoose.model<IComment>('Comment', CommentSchema);

使用数据库的博客 API 路由

// src/app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/database';
import Post from '@/models/Post';
import Category from '@/models/Category';

// 处理 GET 请求 - 获取文章列表
export async function GET(request: Request) {
  try {
    await connectToDatabase();
    
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get('page') || '1');
    const limit = parseInt(searchParams.get('limit') || '10');
    const categoryId = searchParams.get('categoryId') || '';
    const tag = searchParams.get('tag') || '';
    const search = searchParams.get('search') || '';
    
    // 构建查询条件
    const query: any = { published: true };
    
    if (categoryId) {
      query.categoryId = categoryId;
    }
    
    if (tag) {
      query.tags = { $in: [tag] };
    }
    
    if (search) {
      query.$text = { $search: search };
    }
    
    // 查询文章
    const posts = await Post.find(query)
      .select('title excerpt slug categoryId tags author createdAt views likes')
      .sort({ createdAt: -1 })
      .skip((page - 1) * limit)
      .limit(limit)
      .populate('categoryId', 'name slug');
    
    // 获取总数
    const totalItems = await Post.countDocuments(query);
    
    return NextResponse.json({
      success: true,
      data: {
        posts,
        pagination: {
          currentPage: page,
          totalPages: Math.ceil(totalItems / limit),
          totalItems,
          itemsPerPage: limit
        }
      },
      message: '获取文章列表成功'
    });
  } catch (err) {
    console.error('获取文章列表失败:', err);
    return NextResponse.json({
      success: false,
      data: null,
      message: '获取文章列表失败',
      errorCode: 'FETCH_POSTS_FAILED'
    }, { status: 500 });
  }
}

// 处理 POST 请求 - 创建新文章
export async function POST(request: Request) {
  try {
    await connectToDatabase();
    
    // 在实际应用中,这里应该验证用户身份
    // const userId = await verifyAuthToken(request);
    
    const body = await request.json();
    
    // 验证必需字段
    if (!body.title || !

评论