本文将带你一步步实现一个基于 Next.js 14 的现代博客系统,包含完整代码示例和最佳实践。
项目初始化与基础配置
1.1 创建项目
npx create-next-app@latest my-blog --typescript --tailwind --eslint
cd my-blog
1.2 目录结构设置
my-blog/
├── app/
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ ├── (subpages)/ # 子页面分组
│ │ ├── about/
│ │ │ └── page.tsx # 关于页面
│ │ ├── blog/
│ │ │ ├── [slug]/ # 动态博客页面
│ │ │ │ ├── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ └── page.tsx # 博客列表页面
│ │ └── components/ # 共享组件
├── components/ # 全局组件
├── lib/ # 工具函数
├── posts/ # markdown文章
└── public/ # 静态资源
核心页面实现
2.1 根布局设置
// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: {
template: '%s | My Blog',
default: 'My Blog',
},
description: '技术博客分享',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body className={inter.className}>
<main className="max-w-4xl mx-auto px-4">
{children}
</main>
</body>
</html>
)
}
2.2 首页实现
// app/page.tsx
import Link from 'next/link'
import { getPosts } from '@/lib/posts'
export default async function HomePage() {
const posts = await getPosts()
return (
<div className="py-8">
<h1 className="text-4xl font-bold mb-8">我的博客</h1>
<div className="grid gap-6">
{posts.map((post) => (
<article key={post.slug} className="p-6 bg-white rounded-lg shadow">
<Link href={`/blog/${post.slug}`}>
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600">{post.description}</p>
<time className="text-sm text-gray-500">
{new Date(post.date).toLocaleDateString('zh-CN')}
</time>
</Link>
</article>
))}
</div>
</div>
)
}
Markdown 博客系统实现
3.1 文章数据获取
// lib/posts.ts
import matter from 'gray-matter'
import path from 'path'
import fs from 'fs/promises'
import { cache } from 'react'
export type Post = {
slug: string
title: string
date: string
description: string
body: string
}
export const getPosts = cache(async () => {
const postsDirectory = path.join(process.cwd(), 'posts')
const files = await fs.readdir(postsDirectory)
const posts = await Promise.all(
files
.filter((file) => path.extname(file) === '.mdx')
.map(async (file) => {
const filePath = path.join(postsDirectory, file)
const fileContent = await fs.readFile(filePath, 'utf8')
const { data, content } = matter(fileContent)
return {
slug: path.basename(file, '.mdx'),
title: data.title,
date: data.date,
description: data.description,
body: content,
} as Post
})
)
return posts.sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime()
})
})
export async function getPost(slug: string) {
const posts = await getPosts()
return posts.find((post) => post.slug === slug)
}
3.2 博客详情页实现
// app/(subpages)/blog/[slug]/page.tsx
import { getPost, getPosts } from '@/lib/posts'
import { PostBody } from './components/post-body'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return notFound()
return {
title: post.title,
description: post.description,
}
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return notFound()
return (
<article className="py-8 prose prose-lg max-w-none">
<h1>{post.title}</h1>
<time className="text-gray-500">
{new Date(post.date).toLocaleDateString('zh-CN')}
</time>
<PostBody>{post.body}</PostBody>
</article>
)
}
3.3 Markdown 渲染组件
// app/(subpages)/blog/[slug]/components/post-body.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import { Code } from 'bright'
const components = {
pre: Code,
// 自定义其他组件...
}
export function PostBody({ children }: { children: string }) {
return (
<MDXRemote
source={children}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
},
}}
components={components}
/>
)
}
SEO 和性能优化
4.1 站点地图生成
// app/sitemap.ts
import { getPosts } from '@/lib/posts'
export default async function sitemap() {
const posts = await getPosts()
const baseUrl = 'https://your-domain.com'
const postUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
}))
const routes = ['', '/about', '/blog'].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
}))
return [...routes, ...postUrls]
}
4.2 RSS 订阅支持
// scripts/generate-rss.ts
import fs from 'fs'
import RSS from 'rss'
import { getPosts } from '../lib/posts'
async function generateRssFeed() {
const posts = await getPosts()
const site_url = 'https://your-domain.com'
const feedOptions = {
title: '我的技术博客',
description: '分享 Web 开发技术与经验',
site_url: site_url,
feed_url: `${site_url}/feed.xml`,
language: 'zh-CN',
}
const feed = new RSS(feedOptions)
posts.forEach((post) => {
feed.item({
title: post.title,
description: post.description,
url: `${site_url}/blog/${post.slug}`,
date: post.date,
})
})
fs.writeFileSync('./public/feed.xml', feed.xml({ indent: true }))
}
generateRssFeed()
部署配置
5.1 生产环境配置
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 开启图片优化
images: {
domains: ['your-image-domain.com'],
},
// 开启静态页面导出
output: 'export',
// 禁用未使用的功能
experimental: {
appDir: true,
},
}
module.exports = nextConfig
5.2 Vercel 部署
在 Vercel 上创建新项目
连接 GitHub 仓库
配置环境变量(如果需要)
部署命令:
# package.json
{
"scripts": {
"build": "next build",
"postbuild": "node scripts/generate-rss.ts"
}
}
六、实用功能扩展
6.1 评论系统集成
// components/comments.tsx
'use client'
import Giscus from '@giscus/react'
export default function Comments() {
return (
<Giscus
repo="username/repo"
repoId="R_xxx"
category="Comments"
categoryId="xxx"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
theme="light"
/>
)
}
6.2 阅读时间估算
// lib/readingTime.ts
export function getReadingTime(content: string) {
const wordsPerMinute = 200
const wordCount = content.trim().split(/\s+/).length
const readingTime = Math.ceil(wordCount / wordsPerMinute)
return `${readingTime} min read`
}
使用建议
开发流程
使用 TypeScript 保证代码质量
遵循 React Server Components 最佳实践
保持组件的单一职责
性能优化
使用
cache()
缓存数据获取合理使用客户端和服务端组件
实现增量静态再生成(ISR)
内容管理
建立清晰的文章分类系统
使用 Git 进行内容版本控制
定期备份数据
常见问题解决
图片优化
// next.config.js
const nextConfig = {
images: {
domains: ['your-domain.com'],
formats: ['image/avif', 'image/webp'],
},
}
开发环境热更新
// next.config.js
const nextConfig = {
webpack: (config) => {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
}
return config
},
}
如果你觉得这篇文章有帮助,欢迎点赞、关注,后续会持续更新更多 Next.js 相关内容!