博客文章

本项目的博客系统是如何构建的

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

这些依赖的作用:

二、配置文件修改

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 样式

在博客文章详情页中,使用 proseprose-lg 类来应用排版样式:

<div className="prose prose-lg max-w-none">
  <Component />
</div>

五、注意事项

  1. 静态生成支持:博客文章页面使用了 generateStaticParams,支持静态生成,可以在构建时预渲染所有文章页面

  2. 元数据提取:博客列表页和文章详情页都实现了简单的元数据(标题、日期、描述)提取功能

  3. Tailwind 版本兼容性:根据项目使用的 Tailwind CSS 版本选择正确的配置方式

  4. 路径别名:确保 tsconfig.json 中配置了 @/* 路径别名,以便正确导入 MDX 文件

六、总结

通过以上步骤,我们成功为 Next.js 项目添加了完整的 MDX 博客系统,包括:

现在你可以开始在 content/blog/ 目录中创建 MDX 博客文章了!