Using Next.js 14 to Render Markdown Data
Introduction
🔗To implement Markdown document rendering in Next.js 14, there are many packages available. After research, Contentlayer
is commonly recommended for its lightweight and ease of use, but there is currently no version for Next.js 14. Therefore, the widely used react-markdown
, along with gray-matter
, rehype-highlight
, rehype-raw
, and remark-gfm
, was chosen to implement basic MD rendering pages.
First, install the required packages:
bashnpm install react-markdown gray-matter rehype-highlight rehype-raw remark-gfm
Purpose Introduction:
react-markdown: Converts Markdown content into HTML elements.
gray-matter: Parses Markdown strings containing front matter, separating the front matter and Markdown content so that information from the front matter, such as titles, dates, tags, etc., can be used during rendering.
rehype-highlight: A Rehype plugin for adding syntax highlighting when rendering Markdown documents. It supports various programming languages and is essential for articles sharing code, as it automatically identifies and highlights code sections in the article.
rehype-raw: Also a Rehype plugin, used for handling raw HTML in Markdown. It allows HTML tags to be included and rendered in Markdown documents.
When writing Markdown documents, if you need to insert images and adjust their aspect ratio, it's not possible with pure Markdown syntax. When building websites with Hugo, I used shortcodes to handle this issue. After research, it's actually possible to embed HTML syntax directly in Markdown documents and then use rehype-raw for conversion and rendering, which is very fast and convenient.
remark-gfm: A Remark plugin for supporting GitHub Flavored Markdown (GFM) extended syntax. This includes tables, automatic links, strikethroughs, and more.
Implementation
🔗Retrieving Original MD Files
🔗The main functionality is to render existing Markdown documents. Since the original MD files were not stored in a single folder, I chose to retrieve all files recursively.
Next, use gray-matter
to parse all files and return them.
Path: src\lib\posts.js
JSimport fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const postsDirectory = path.join(process.cwd(), 'posts');
// Recursive function to retrieve all Markdown files in a directory and its subdirectories
function getMarkdownFiles(dir, fileList) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
fileList = getMarkdownFiles(path.join(dir, file), fileList);
} else if (path.extname(file) === '.md') {
fileList.push(path.join(dir, file));
}
});
return fileList;
}
export function getSortedPostsData() {
const isDev = process.env.ENV === 'dev';
// Get file names under /posts
// const fileNames = fs.readdirSync(postsDirectory);
// Retrieve all Markdown files including those in subdirectories
const filePaths = getMarkdownFiles(postsDirectory, []);
let allPostsData = filePaths.map((filePath) => {
// Remove ".md" from file name to get id
const id = path.parse(filePath).name;
const fileContents = fs.readFileSync(filePath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Combine the data with the id
return {
id,
filePath,
...matterResult.data,
};
});
if (!isDev) {
allPostsData = allPostsData.filter(post => post.draft != true);
}
// Sort posts by date
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1;
} else {
return -1;
}
});
}
Detailed Explanation:
-
const postsDirectory = path.join(process.cwd(), 'posts');
Retrieves the path to theposts
folder. -
const fileNames = fs.readdirSync(postsDirectory);
If MD files are only placed in theposts
layer, this can be used directly. -
getMarkdownFiles()
Uses recursion to retrieve all Markdown files, including those in subdirectories, due to MD files being scattered across different subfolders. -
getSortedPostsData()
To obtain the data for each article, including the front matter of the article. -
Additionally,
isDev
is used to determine if it's only in the testing environment, displaying data wheredraft
isfalse
(articles not yet intended to be published). -
Additionally, returning the
filePath
parameter because some of my file names are duplicates, so the path had to be used as an ID ultimately.
With the above, this function can be used to obtain the corresponding article data in the article list or article screen.
Next, we will explain how to render the content of MD documents.
Displaying Article Content
🔗By using getSortedPostsData()
, you can get all the data of the articles. The front matter can be displayed directly. To display the content, since it is in Markdown syntax, react-markdown
is used to convert it into HTML elements.
Code
JS
export default async function Page({ params }: { params: { lang: Locale, category: string, post: string } }) {
const { layout } = await getDictionary(params.lang)
let post: any = getPostData(params.lang, params.category + "/" + params.post);
const content = post.content;
post = post.data;
return (
<article className="flex flex-col items-center justify-between min-h-screen max-w-full p-4">
<div className="mdx max-w-screen-sm w-full break-normal">
<h1 className="text-3xl font-bold my-6">
{post.title}
</h1>
<hr />
<div className="pt-4 pb-4">
{post.description}
</div>
<ReactMarkdown
rehypePlugins={[rehypeHighlight, rehypeRaw, rehypeSlug]}
components={components}
remarkPlugins={[[remarkGfm, { singleTilde: false }], [remarkToc]]}
>
{content}
</ReactMarkdown>
</div>
</article>
);
}
//The following are adjustments for specific conditions
const components = {
img: CustomImage,
iframe: CustomIframe,
code: CodeBlock,
h1: (props: any) => <CustomHeading level={1} {...props} />,
h2: (props: any) => <CustomHeading level={2} {...props} />,
h3: (props: any) => <CustomHeading level={3} {...props} />,
h4: (props: any) => <CustomHeading level={4} {...props} />,
h5: (props: any) => <CustomHeading level={5} {...props} />,
h6: (props: any) => <CustomHeading level={6} {...props} />,
};
const CustomImage = ({ src, alt, title, width, link }: any) => {
return (
<div className="w-full flex flex-col items-center justify-center mt-3 mb-3">
<div style={{ maxWidth: width, width: 'auto' }}>
<Image src={src} alt={alt} title={title} width={800} height={800} />
</div>
<span>{title}</span>
{link && <Link href={link}>Source</Link>}
</div>
);
};
const CustomIframe = ({ src }: any) => {
return (
<div className="max-w-full border-spacing-0 flex justify-center">
<iframe src={src}
width="100%"
height="auto"
allowFullScreen
referrerPolicy="no-referrer-when-downgrade"
loading="lazy">
</iframe>
</div>
);
};
const CodeBlock = ({ node, inline, className, children, ...props }: any) => {
const match = /language-([\w-#]+)/.exec(className || '');
return !inline && match ? (
<>
<div className="bg-background px-2 py-1 text-sm font-mono">
<div className="bg-gray-300 text-black inline-block rounded-md px-2">
{match[1]}
</div>
</div>
<pre className={`${className} max-w-full overflow-auto rounded-md p-5`} {...props}>
<code>{children}</code>
</pre>
</>
) : (
<code className={`${className} break-words bg-gray-400 text-rose-700 rounded-md`} {...props}>
{children}
</code>
);
};
const CustomHeading = ({ level, children }: { level: number, children: React.ReactNode }) => {
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
let Fragment = "";
if (typeof children === 'string') {
Fragment = children as string;
Fragment = Fragment.replace(/ /g, '-');
}
return (
<>
{/* Hidden anchor for scroll positioning /}
<div id={Fragment} className="relative block -top-20 invisible" />
{/ Title with links */}
<div className="flex items-center group space-x-2">
<HeadingTag >{children}</HeadingTag>
<Link href={Fragment && `#${Fragment}`}
className="mt-4 inline-flex h-6 w-6 items-center justify-center text-lg text-slate-400 no-underline opacity-0 transition-opacity group-hover:opacity-100 dark:text-slate-400 dark:group-hover:text-slate-300"
aria-label="Anchor"
>
🔗
</Link>
</div>
</>
);
};
Detailed Explanation:
This code displays a specific article. Since my website includes multiple languages, there is a lang
parameter that can be ignored for now. The main logic uses the category
and the article title post
to retrieve the content of a specific article.
getPostData()
is a function located in src\lib\posts.js
used to obtain the content of a specific article. The logic is simple: it retrieves all articles using getSortedPostsData()
and then filters out the corresponding article based on the given conditions. As for performance, since I've decided to create SSG static pages for each article using generateStaticParams()
, the actual website performance is very fast.
Next, just use
JS<ReactMarkdown>
{content}
</ReactMarkdown>
This allows for the rendering of Markdown content.
Of course, not all Markdown features can be rendered directly, including tables, automatic links, strikethroughs, or if there are code blocks, and the appearance does not match what you had in mind, you might consider extending functionality.
JS<ReactMarkdown
rehypePlugins={[rehypeHighlight, rehypeRaw, rehypeSlug]}
components={components}
remarkPlugins={[[remarkGfm, { singleTilde: false }], [remarkToc]]}>
{content}
</ReactMarkdown>
-
rehypePlugins
Includes the code highlight extensionrehypeHighlight
, the conversion of HTML syntax contained in Markdown documentsrehypeRaw
, and automatically adds corresponding IDs to each HeadingrehypeSlug
. -
remarkPlugins
IncludesremarkGfm
for displaying table styles, andremarkToc
for automatically displaying a table of contents. -
components
Allows for additional decoration of specific HTML to enhance aesthetics. You can see that I have made additional adjustments to content like images and iframes, but I will not describe the detailed usage in this article.
Conclusion
🔗This article mainly explains how to retrieve MD files from the root directory and the process of rendering them with ReactMarkdown, as well as briefly describing how to adjust some appearances, hoping it helps you.
Alvin
Software engineer, interested in financial knowledge, health concepts, psychology, independent travel, and system design.
Related Posts
Discussion (0)
No comments yet.