Next.js 13 App Router搭建简易博客:MDX方案困惑求助
Next.js 13.4 App Router 本地MDX博客实现方案
一、首页:无额外依赖提取MDX元数据
Next.js 13+ App Router允许在服务器组件中直接用fs/promises和path模块读取文件,无需引入gray-matter。MDX的frontmatter是文件顶部的YAML块,可通过正则匹配手动提取:
- 在
app/blog/page.tsx(服务器组件)中遍历content/blog目录下的MDX文件:
import fs from 'fs/promises'; import path from 'path'; export default async function BlogHome() { const postsDir = path.join(process.cwd(), 'content/blog'); const fileNames = await fs.readdir(postsDir); const posts = await Promise.all( fileNames.map(async (fileName) => { const filePath = path.join(postsDir, fileName); const fileContent = await fs.readFile(filePath, 'utf8'); // 正则匹配提取frontmatter const frontmatterMatch = fileContent.match(/^---\n([\s\S]*?)\n---/); if (!frontmatterMatch) return null; const frontmatterStr = frontmatterMatch[1]; // 手动解析简单YAML结构 const frontmatter = frontmatterStr.split('\n').reduce((acc, line) => { const [key, ...valueParts] = line.split(': '); if (key) acc[key.trim()] = valueParts.join(': ').trim(); return acc; }, {} as { title: string; date: string; slug: string }); return { ...frontmatter, slug: fileName.replace('.mdx', '') }; }) ); // 过滤无效数据并按日期倒序排序 const validPosts = posts.filter(Boolean).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); return ( <div> <h1>博客首页</h1> <ul> {validPosts.map((post) => ( <li key={post.slug}> <a href={`/blog/${post.slug}`}>{post.title}</a> <p>{post.date}</p> </li> ))} </ul> </div> ); }
若frontmatter有复杂结构(如数组、嵌套对象),再考虑引入轻量的
yaml包,简单场景下手动解析足够满足需求。
二、博客详情页:动态slug渲染MDX
结合@next/mdx和动态路由可实现动态slug场景的MDX渲染:
- 安装必要依赖:
npm install @next/mdx @mdx-js/loader @mdx-js/react next-mdx-remote
- 在
next.config.js中配置MDX支持:
/** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], }; const withMDX = require('@next/mdx')(); module.exports = withMDX(nextConfig);
- 创建动态路由
app/blog/[slug]/page.tsx,读取并渲染对应MDX文件:
import fs from 'fs/promises'; import path from 'path'; import { MDXRemote } from 'next-mdx-remote/rsc'; type Props = { params: { slug: string }; }; export default async function BlogPost({ params }: Props) { const { slug } = params; const postsDir = path.join(process.cwd(), 'content/blog'); const filePath = path.join(postsDir, `${slug}.mdx`); try { const fileContent = await fs.readFile(filePath, 'utf8'); // 分离frontmatter与MDX内容 const [_, frontmatterStr, mdxContent] = fileContent.split(/^---\n([\s\S]*?)\n---\n/); const frontmatter = frontmatterStr.split('\n').reduce((acc, line) => { const [key, ...valueParts] = line.split(': '); if (key) acc[key.trim()] = valueParts.join(': ').trim(); return acc; }, {} as { title: string; date: string }); return ( <div className="max-w-3xl mx-auto p-4"> <h1>{frontmatter.title}</h1> <p className="text-gray-500">{frontmatter.date}</p> <div className="mt-6 prose"> <MDXRemote source={mdxContent} /> </div> </div> ); } catch (err) { return <div>文章不存在</div>; } } // 生成静态路径,构建时预渲染所有文章 export async function generateStaticParams() { const postsDir = path.join(process.cwd(), 'content/blog'); const fileNames = await fs.readdir(postsDir); return fileNames.map((fileName) => ({ slug: fileName.replace('.mdx', ''), })); }
用
next-mdx-remote/rsc的MDXRemote组件可在服务器组件中动态渲染MDX内容,完美适配动态slug场景。
三、统一布局实现
在app/blog/layout.tsx中定义博客板块的统一布局:
export default function BlogLayout({ children }: { children: React.ReactNode }) { return ( <div className="min-h-screen bg-gray-50"> <header className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 py-4"> <a href="/blog" className="text-xl font-bold">我的博客</a> </div> </header> <main className="max-w-7xl mx-auto px-4 py-8"> {children} </main> </div> ); }
这套方案满足:
- 本地MDX文件作为文章源,编辑便捷
- 首页自动提取所有文章元数据并排序展示
- 动态slug渲染对应文章,布局统一
- 构建时预渲染所有文章,适配低流量场景
内容的提问来源于stack exchange,提问作者Dave Feldman
相关产品推荐
相关产品推荐

