如何在Next.js中展示嵌套分类文件夹内的Markdown文件
解决方案
你可以用 Next.js 的 Catch-all 动态路由 实现无需手动创建分类路由文件的需求,同时优化文件读取逻辑适配任意层级的嵌套分类,具体实现步骤如下:
第一步:调整路由文件结构
将原 pages/articles/[slug].js 重命名为 pages/articles/[...slug].js,这种带三个点的动态路由可以匹配任意层数的路径:
- 访问
/articles/CategoryOne/markdown-one-1时,params.slug值为['CategoryOne', 'markdown-one-1'] - 访问
/articles/CategoryTwo/subcategory/test时,params.slug值为['CategoryTwo', 'subcategory', 'test']
自动适配任意深度的分类嵌套,不需要手动创建对应分类目录的路由文件。
第二步:优化工具函数(支持任意深度递归遍历)
替换 utils/index.js 中的逻辑,用递归方法读取 /posts 下所有层级的 .md 文件,不需要限制分类只有两层:
import fs from 'fs' import path from 'path' import matter from 'gray-matter' const postsRoot = path.join(process.cwd(), 'posts') // 递归遍历所有目录下的md文件 export function getAllPosts() { const posts = [] // 递归遍历函数 function traverseDir(currentPath, relativePath = []) { const entries = fs.readdirSync(currentPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(currentPath, entry.name) if (entry.isDirectory()) { // 是目录就继续递归,记录相对路径 traverseDir(fullPath, [...relativePath, entry.name]) } else if (entry.isFile() && entry.name.endsWith('.md')) { // 是md文件就解析数据 const slug = entry.name.replace('.md', '') const fullSlug = [...relativePath, slug] const markdownContent = fs.readFileSync(fullPath, 'utf-8') const { data: frontmatter } = matter(markdownContent) posts.push({ fullSlug, // 完整路径段数组,用于生成路由 slugStr: fullSlug.join('/'), // 字符串形式的路径,用于列表展示跳转 frontmatter, relativePath: path.join(...relativePath, entry.name) // 相对posts根目录的路径 }) } } } traverseDir(postsRoot) return posts }
第三步:优化文章列表页 pages/articles/index.js
直接调用工具函数获取全量文章,不需要硬编码分类名称,新增分类时不需要修改此处代码:
import React from 'react' import Link from 'next/link' import { getAllPosts } from '../../utils' import { useRouter } from 'next/router' export default function Blog({ allPosts }) { const router = useRouter() if (router.isFallback) { return <div>Loading...</div> } return ( <div> <h1>所有文章</h1> {/* 直接遍历所有文章即可,按分类分组也可自行处理 */} {allPosts.map(post => ( <div key={post.slugStr}> <Link href={`/articles/${post.slugStr}`}> <a> <h2>{post.frontmatter.title}</h2> <p>分类:{post.fullSlug.slice(0, -1).join(' / ')}</p> <p>{post.frontmatter.date} • {post.frontmatter.reading_time}</p> </a> </Link> </div> ))} </div> ) } export async function getStaticProps() { const allPosts = getAllPosts() return { props: { allPosts }, } }
第四步:优化文章详情页 pages/articles/[...slug].js
修改 getStaticPaths 生成全量文章的路径,getStaticProps 按路径读取对应文件:
import React from 'react' import fs from 'fs' import path from 'path' import matter from 'gray-matter' import marked from 'marked' import { useRouter } from 'next/router' import { getAllPosts } from '../../utils' const postsRoot = path.join(process.cwd(), 'posts') export default function PostPage({ frontmatter: { title, date, reading_time, category }, content, }) { const router = useRouter() if (router.isFallback) { return <div>Loading...</div> } return ( <div> <h1>{title}</h1> <p>{date} • {reading_time}</p> <div dangerouslySetInnerHTML={{__html: marked(content)}}></div> </div> ) } export async function getStaticPaths() { const allPosts = getAllPosts() const paths = allPosts.map(post => ({ params: { slug: post.fullSlug } })) return { paths, fallback: false, } } export async function getStaticProps({ params: { slug } }) { // slug数组最后一段是文件名,前面是分类路径 const fileName = `${slug.at(-1)}.md` const relativeDir = slug.slice(0, -1).join(path.sep) const fullPath = path.join(postsRoot, relativeDir, fileName) const markdownWithMeta = fs.readFileSync(fullPath, 'utf-8') const { data: frontmatter, content } = matter(markdownWithMeta) return { props: { frontmatter, content, }, } }
注意事项
- 该方案支持任意深度的分类嵌套,后续新增分类仅需要在
/posts目录下新建对应文件夹,放入md文件即可,无需修改路由或读取逻辑 - 不同分类下的同名md文件不会出现路径冲突,因为路由中携带了完整分类路径
- 如果需要支持分类列表页(比如访问
/articles/CategoryOne展示该分类下所有文章),可以将路由文件改为可选捕获路由[[...slug]].js,自行适配分类列表的渲染逻辑即可
内容的提问来源于stack exchange,提问作者ParkerIndustries
相关产品推荐
相关产品推荐

