You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 02:27:01