博客文章
本项目的博客系统是如何构建的
date: 2026-04-20 description: Next.js 项目中添加 MDX 博客系统指南。详细记录如何在 Next.js 16 项目中集成 MDX 博客系统,包括必要的依赖安装、配置文件修改和博客页面实现
前言
MDX 是一种强大的内容格式,它允许你在 Markdown 中直接使用 React 组件。本指南将详细记录如何为 Next.js 16 项目添加完整的 MDX 博客系统。
一、依赖安装
首先需要安装 MDX 相关的依赖包:
pnpm add @next/mdx @mdx-js/loader @mdx-js/react
pnpm add -D @tailwindcss/typography
这些依赖的作用:
- @next/mdx:Next.js 官方提供的 MDX 集成方案
- @mdx-js/loader 和 @mdx-js/react:MDX 核心包,提供编译和运行时支持
- @tailwindcss/typography:Tailwind CSS 的排版插件,提供 prose 类样式
二、配置文件修改
2.1 更新 next.config.mjs
在项目根目录的 next.config.mjs 中添加 MDX 配置:
import createMDX from '@next/mdx'
const withMDX = createMDX({
extension: /\.(md|mdx)$/,
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// ... 其他配置
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}
export default withMDX(nextConfig)
2.2 创建 mdx-components.tsx
在项目根目录创建 mdx-components.tsx 文件,这是 App Router 使用 MDX 所必需的:
import type { MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
}
}
2.3 配置 Tailwind CSS Typography
根据项目使用的 Tailwind CSS 版本不同,配置方式也不同:
对于 Tailwind CSS v4(本项目使用的版本),需要在 styles/globals.css 中添加:
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@import 'tw-animate-css';
对于 Tailwind CSS v3,需要创建 tailwind.config.ts:
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,md,mdx}',
'./pages/**/*.{js,ts,jsx,tsx,md,mdx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./content/**/*.{md,mdx}',
],
plugins: [
require('@tailwindcss/typography'),
],
}
export default config
三、创建博客系统结构
3.1 目录结构
项目根目录/
├── app/
│ ├── blog/
│ │ ├── [slug]/
│ │ │ └── page.tsx # 博客文章详情页
│ │ └── page.tsx # 博客列表页
├── content/
│ └── blog/ # MDX 博客文章存放目录
├── mdx-components.tsx # MDX 组件配置
└── next.config.mjs # Next.js 配置
3.2 创建博客列表页
app/blog/page.tsx:
import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: '博客',
description: '博客描述',
};
interface BlogPost {
slug: string;
title: string;
date: string;
description: string;
}
function getBlogPosts(): BlogPost[] {
const blogDir = join(process.cwd(), 'content', 'blog');
const files = readdirSync(blogDir).filter((file) => file.endsWith('.mdx'));
return files.map((file) => {
const slug = file.replace('.mdx', '');
const content = readFileSync(join(blogDir, file), 'utf8');
// 简单的元数据提取
const titleMatch = content.match(/^#\s+(.*)$/m);
const dateMatch = content.match(/^date:\s+(.*)$/m);
const descriptionMatch = content.match(/^description:\s+(.*)$/m);
return {
slug,
title: titleMatch ? titleMatch[1] : slug,
date: dateMatch ? dateMatch[1] : '',
description: descriptionMatch ? descriptionMatch[1] : '',
};
});
}
export default function BlogPage() {
const posts = getBlogPosts();
return (
<main className="flex-1 container mx-auto px-6 py-12">
<h1 className="text-6xl font-black tracking-tighter uppercase italic">
博客
</h1>
<div className="space-y-8">
{posts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<h2 className="text-2xl font-bold mb-2">{post.title}</h2>
<p className="text-zinc-500 text-sm mb-4">{post.date}</p>
<p className="text-zinc-700">{post.description}</p>
</Link>
))}
</div>
</main>
);
}
3.3 创建博客文章详情页
app/blog/[slug]/page.tsx:
import { readFileSync } from 'fs';
import { join } from 'path';
import { notFound } from 'next/navigation';
import { Metadata } from 'next';
interface Params {
slug: string;
}
async function getBlogPost(slug: string) {
const blogDir = join(process.cwd(), 'content', 'blog');
const filePath = join(blogDir, `${slug}.mdx`);
try {
return readFileSync(filePath, 'utf8');
} catch {
return null;
}
}
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { slug } = await params;
const content = await getBlogPost(slug);
if (!content) notFound();
const titleMatch = content.match(/^#\s+(.*)$/m);
const descriptionMatch = content.match(/^description:\s+(.*)$/m);
return {
title: titleMatch ? titleMatch[1] : slug,
description: descriptionMatch ? descriptionMatch[1] : '',
};
}
export async function generateStaticParams() {
const { readdirSync } = await import('fs');
const blogDir = join(process.cwd(), 'content', 'blog');
const files = readdirSync(blogDir).filter((file) => file.endsWith('.mdx'));
return files.map((file) => ({
slug: file.replace('.mdx', ''),
}));
}
export default async function BlogPostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const content = await getBlogPost(slug);
if (!content) notFound();
const { default: Component } = await import(`@/content/blog/${slug}.mdx`);
return (
<main className="flex-1 container mx-auto px-6 py-12">
<div className="mb-12 border-b-4 border-black pb-8">
<h1 className="text-6xl font-black tracking-tighter uppercase italic">
博客文章
</h1>
</div>
<div className="prose prose-lg max-w-none">
<Component />
</div>
</main>
);
}
3.4 创建示例博客文章
在 content/blog/ 目录中创建 MDX 文件:
# 我的第一篇博客
date: 2026-04-20
description: 这是博客文章的描述
## 标题二
这是博客正文内容,支持 **Markdown** 语法。
- 列表项一
- 列表项二
> 引用内容
也可以使用 React 组件:
import { Button } from '@/components/ui/button'
<Button>点击我</Button>
四、MDX 博客文章页面使用 prose 样式
在博客文章详情页中,使用 prose 和 prose-lg 类来应用排版样式:
<div className="prose prose-lg max-w-none">
<Component />
</div>
五、注意事项
-
静态生成支持:博客文章页面使用了
generateStaticParams,支持静态生成,可以在构建时预渲染所有文章页面 -
元数据提取:博客列表页和文章详情页都实现了简单的元数据(标题、日期、描述)提取功能
-
Tailwind 版本兼容性:根据项目使用的 Tailwind CSS 版本选择正确的配置方式
-
路径别名:确保
tsconfig.json中配置了@/*路径别名,以便正确导入 MDX 文件
六、总结
通过以上步骤,我们成功为 Next.js 项目添加了完整的 MDX 博客系统,包括:
- MDX 文件的编译和支持
- Tailwind CSS Typography 插件的集成
- 博客列表页和文章详情页的实现
- 静态生成优化
现在你可以开始在 content/blog/ 目录中创建 MDX 博客文章了!