如何在Next.js项目中从Markdown前置元数据渲染文本与列表?
在Next.js中实现Markdown驱动的动态博客页面(含图片与前置元数据)
一、编写带前置元数据的Markdown文件
你的Markdown文件必须用---包裹YAML格式的前置元数据,把图片和focusAreas列表都纳入其中,示例如下:
--- title: React Native跨平台项目案例 coverImages: - https://i.sstatic.net/TiLcHLJj.png - https://i.sstatic.net/DdsqRta4.png focusAreas: - Implemented a scalable React Native app with TypeScript for cross-platform support - React Navigation for seamless routing - Redux Toolkit for state management - React Query for data fetching - Styled Components for consistent UI - Created reusable UI components - Developed personalization engine --- # 项目背景 这里可以编写博客正文内容,比如项目需求、实现难点、成果总结等...
注意:YAML格式需严格对齐,数组项的-后要加空格,图片地址直接存入coverImages数组即可。
二、Next.js动态页面实现
1. 安装依赖
先安装解析Markdown的工具包:
npm install gray-matter remark remark-html
2. 编写数据读取函数
在项目根目录新建lib/getCaseStudyData.ts,用于读取Markdown文件并解析前置元数据:
import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import { remark } from 'remark'; import html from 'remark-html'; // Markdown文件存放目录,可根据实际路径调整 const caseStudiesDir = path.join(process.cwd(), 'case-studies'); export async function getCaseStudyData(slug: string) { const fullPath = path.join(caseStudiesDir, `${slug}.md`); const fileContents = fs.readFileSync(fullPath, 'utf8'); // 解析前置元数据 const matterResult = matter(fileContents); // 将Markdown正文转换为HTML const processedContent = await remark() .use(html) .process(matterResult.content); const contentHtml = processedContent.toString(); return { slug, contentHtml, frontmatter: matterResult.data as { title: string; coverImages: string[]; focusAreas: (string | Record<string, string[]>)[]; }, }; }
3. 修改动态页面组件(/[slug]/page.tsx)
修复原列表渲染逻辑,同时添加图片展示功能:
import { getCaseStudyData } from '@/lib/getCaseStudyData'; // 生成静态路径(用于静态站点生成,可选) export async function generateStaticParams() { const fs = require('fs'); const path = require('path'); const caseStudiesDir = path.join(process.cwd(), 'case-studies'); const filenames = fs.readdirSync(caseStudiesDir); return filenames.map((filename: string) => ({ slug: filename.replace('.md', ''), })); } export default async function CaseStudyPage({ params }: { params: { slug: string } }) { const caseStudy = await getCaseStudyData(params.slug); const renderList = (items: (string | Record<string, string[]>)[]) => ( <div className="flex w-5/6 flex-col gap-2 text-[16px] font-[500] leading-7 max-w-[702px]"> {items.map((item, index) => { if (typeof item === 'string') { return <p key={index} className="text-[16px] font-[500] leading-7">{item}</p>; } // 处理嵌套列表:提取主项和子列表 const mainItem = Object.keys(item)[0]; const subItems = Object.values(item)[0]; return ( <ul key={index} className="list-disc px-5"> <li>{mainItem}</li> {subItems.length > 0 && renderList(subItems)} </ul> ); })} </div> ); return ( <div className="max-w-4xl mx-auto p-8"> <h1 className="text-3xl font-bold mb-6">{caseStudy.frontmatter.title}</h1> {/* 展示两张图片 */} <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"> {caseStudy.frontmatter.coverImages.map((imgUrl, idx) => ( <img key={idx} src={imgUrl} alt={`项目截图${idx+1}`} className="w-full h-auto rounded-lg shadow-md" /> ))} </div> {/* 渲染focusAreas列表 */} <h2 className="text-2xl font-semibold mb-4">核心工作内容</h2> {renderList(caseStudy.frontmatter.focusAreas)} {/* 渲染Markdown正文 */} <div className="mt-8 prose max-w-none"> <div dangerouslySetInnerHTML={{ __html: caseStudy.contentHtml }} /> </div> </div> ); }
三、关键修复说明
- 原
renderList函数未将嵌套列表的主项放入<li>标签,调整后符合HTML语义规范; - 新增图片展示模块,从前置元数据的
coverImages数组读取地址,用Grid布局适配移动端与桌面端; - 补充TypeScript类型定义,避免类型报错。
内容的提问来源于stack exchange,提问作者Prongs
相关产品推荐
相关产品推荐

