In this article, we'll take a look at what Markdown and MDX are. We’ll also discuss how to easily integrate Markdown with MDX in a modern Next.js application.
Markdown’s syntax was introduced and widely adopted as a web writing format for this reason—it offers a clear and easy-to-use method for formatting plain text in online applications.
MDX, an evolution of Markdown often referred to as “Markdown for the component era,” extends traditional Markdown by allowing JSX components to be embedded within Markdown files. This enhances benefits like code reusability, improved interactivity, dynamism and access to all the features React provides within Markdown content.
In one of its recent major releases, Next.js, a leading React framework, introduced an easy way of working with MDX. This includes support for the App Router and other pre-existing infrastructure.
In this article, we will explore the steps to integrate MDX into modern Next.js applications.
Markdown is a markup language that helps format plain text, making it easy to read and write. It is a tool for converting text into HTML, allowing users to create content in a user-friendly text format.
One of the advantages of Markdown is its syntax, which eliminates the need for manually defining HTML elements. Unlike HTML, where users are required to use tags and attributes to structure content, Markdown simplifies the process with symbols and conventions. Writing in Markdown resembles plain text, making it accessible to people with diverse technical backgrounds. It is commonly used for documentation, blogs and other text-based content.
Some of the benefits of Markdown include:
Markdown’s syntax includes various elements, such as:
# h1 header
## h2 header
### h3 header
Converts to the following in HTML respectively:
{" "}
<h1>h1 header</h1>
<h2>h1 header</h2>
<h3>h1 header</h3>
1. Ordered item 1
2. Ordered item 2
3. Ordered item 3
- Unordered item 1
- Unordered item 2
- Unordered item 3
Converts to:
{" "}
<ol>
<li>Ordered item 1</li>
<li>Ordered item 2</li>
<li>Ordered item 3</li>
</ol>
{" "}
<ul>
<li>Unordered item 1</li>
<li>Unordered item 2</li>
<li>Unordered item 3</li>
</ul>
\[Link Text\](https://www.sample-link.com)
Converts to:
<a href="https://www.sample-link.com">Link Text</a>
!\[Alt text\](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2024/2024-07/path-or-link/to/img.jpg)
Converts to:
<img src="/path-or-link/to/img.jpg" alt="Alt text">
MDX is a superset of Markdown that enables the writing of JSX directly in Markdown files. It combines the simplicity of Markdown syntax with the dynamic capabilities of JSX. It provides a way to infuse interactivity and integrate React components into web content.
It is important to understand the difference between Markdown and MDX. While Markdown focuses primarily on text formatting and basic content structuring, offering a simple solution for creating static content, MDX extends its capabilities by introducing support for JSX components, enabling dynamic and interactive content creation. We can import components, such as interactive charts, calendars and other UI components, and embed them within our content.
MDX files take on a .mdx file extension, unlike the .md extension of Markdown files. Shown below is a code snippet of what a typical MDX file may look like:
import { MyComponent } from "/path-to-my-component";
# Sample MDX file
<MyComponent />
MDX works quite well with popular frontend libraries. However, this article focuses only on its integration with Next.js applications.
Run the command below to set up a new Next.js project:
npx create-next-app
Give the project a preferred name and fill up the subsequent prompts as shown below:
This command installs the latest version of Next.js, which is version 14+ at the time of writing. Run the following command to start the development server:
npm run dev
To work with MDX in Next.js applications, you need to install the @next/mdx
package. This package handles the conversion of Markdown and React components into HTML for smooth integration with Server Components, which are the default in the Next.js App Router.
Run the following command in your terminal:
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
Here, we install @next/mdx along with the following packages:
Create a mdx-components.tsx
file at the root directory of your project and add the following to it:
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
};
}
This file is required to use MDX with App Router and will not work when not included. The useMDXComponents
component provides an interface that maps different components that should be rendered for each HTML element. In a later section, we’ll see how this works.
Lastly, update the next.config.mjs
file at the root directory of the project as shown below:
import nextMDX from "@next/mdx";
const withMDX = nextMDX();
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure `pageExtensions` to include MDX files
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
};
export default withMDX(nextConfig);
Here, we defined the configuration necessary to make our Next.js application recognize MDX files within its structure.
With that done, create a new folder called test-page
in the src/app/ folder
and add a page.mdx
file to it. Open the file and add the following to it:
# This is a test MDX page
Here is a paragraph
\[Link Text\](https://www.test-page.com)
here is a list:
- Item one
- Item two
- Item three
Here, we created an MDX file and added some basic markdown to it.
Open http://localhost:3000/test-page in your browser to see the MDX file rendered as a page.
Don’t worry about the styles for now; we’ll cover how to properly style our MDX files later. However, you can already see the corresponding HTML elements in the developer tools when you inspect the page.
We can also import and render a React component inside an MDX file. Create a new folder called components
in the src/
directory and add a TestComponent.tsx
file to it. Add the following to the file:
import React from "react";
type Props = {};
export default function TestComponent({}: Props) {
return (
<div className="text-xl text-green-600 my-4">This is a React Component</div>
);
}
Next, update the src/app/test-page/page.mdx
file as shown below:
import TestComponent from "../../components/TestComponent.tsx";
# This is a test MDX page
Here is a paragraph
\[Link Text\](https://www.test-page.com)
here is a list:
- Item one
- Item two
- Item three
## This is an h2 element
<TestComponent />
Here, we imported and rendered the TestComponent
React component in an MDX file.
We can also import and render MDX components inside a React component. To quickly test this, update the src/app/page.tsx
file as shown below:
import TestMdx from "@/app/test-page/page.mdx";
export default function Home() {
return <TestMdx />;
}
Here, we removed the template code and rendered the component provided by our MDX file.
When you visit http://localhost:3000/ in your browser, you should see the same content as on http://localhost:3000/test-page.
Recall that we previously created a useMDXComponents
component in this article. This component allows us to map different components to each HTML element, therefore allowing us to customize individual Markdown and MDX elements. In other words, we can customize an element to match our desired appearance.
To do this, update the mdx-components.tsx file at the root level of the application, as shown below:
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: ({ children }) => (
<h1 style={{ fontSize: "32px", color: "red" }}>{children}</h1>
),
...components,
};
}
Here, we mapped all h1 elements in our MDX file to an h1 element with additional styles. Instead of mapping individual elements, Tailwind CSS provides a typography plugin to simplify this process. The @tailwindcss/typography
package provides a set of prose classes that can be used to add beautiful typographic defaults to markdown.
Run the following command in the terminal to install the package:
npm install -D @tailwindcss/typography
Then add the plugin to the tailwind.config.ts
file at the root folder as shown below:
import type { Config } from "tailwindcss";
const config: Config = {
content: [
//...
],
theme: {
//...
},
plugins: [require("@tailwindcss/typography")],
};
export default config;
To use the plugin, we only need to wrap our MDX in an element with the prose class name. Update the src/app/test-page/page.mdx
file as shown below:
import TestComponent from "../../components/TestComponent.tsx";
# This is a test MDX page
Here is a paragraph
\[Link Text\](https://www.test-page.com)
here is a list:
- Item one
- Item two
- Item three
## This is an h2 element
<TestComponent />
export default ({ children }) => <div className="prose">{children}</div
>
Here, we defined a layout for our MDX component and applied the prose class to its div element.
Visit http://localhost:3000/test-page to see the improved markdown formatting. Additionally, comment out the custom mapping for the h1 element in the mdx-components.tsx
file.
The Tailwind plugin also offers some other variations of the prose class. For further details, you can explore the documentation.
In addition to the @next/mdx
package that sources data from local files, there’s also the next-mdx-remote
package. This package allows dynamic fetching and rendering of MDX files stored remotely in a CMS, database, GitHub repository or another location.
Run the command below in your terminal to install the package and expose its default component and functions.
npm install next-mdx-remote
Here is a sample code snippet from the official documentation that shows a simple use case of the next-mdx-remote
package:
import { MDXRemote } from 'next-mdx-remote/rsc'
export default async function RemoteMdxPage() {
// fetch MDX text from a local file, database, CMS, fetch, or anywhere...
const res = await fetch('https://...')
const markdown = await res.text()
return <MDXRemote source={markdown} />
}
The component above fetches markdown text from a remote source and passes it as a source prop to the MDXRemote
component provided by the package. This component sees that the markdown text is formatted correctly.
It’s essential to note that the MDXRemote
component should only be rendered on the server.
We can easily add metadata to MDX files using Frontmatter, which is a YAML-like key/value format for storing data about a page. This metadata can include information like the title, description, date and more. Setting this metadata can help improve your application’s SEO.
To add metadata to an MDX file with Frontmatter, include the data as key/value pairs within a three-dash block at the top of your file, as shown below:
---
title: "A Blog Post"
author: "Mark Adams"
publishDate: "4/2/24"
---
To see how handy this can be, we’ll create a simple blog application in the next section, including metadata and integrating everything we’ve covered in this article.
In this section, we want to build a simple blog for the demo application that would render a list of blog posts, as well as a dynamic page for each blog post.
To get started, open the src/app/page.tsx
file and add the following to it:
export default function Home() {
return (
<section className="p-4 flex flex-col items-center">
<h1 className="text-3xl font-medium">Home Page</h1>
<p>MDX Next.js Integration Demo</p>
</section>
);
}
The code above reformats the entry page of the application to include just a simple text only.
Create a Header.tsx
file in the src/components
folder and add the following to it:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
{children}
</body>
</html>
);
}
Save the changes; you should see the formatted homepage and the header included.
Next, we’ll create two sample blog posts using MDX files. Create a blog-posts
folder within the src/
directory. Inside this new folder, add two files named test-post.mdx
and `another-post.mdx.
Open the test-post.mdx
file and add the following to it:
---
title: "Test Blog Post"
author: "John Doe"
publishDate: "09/06/24"
---
# This is a test blog post
Here is a paragraph
\[Link Text\](https://www.example.com)
here is a list:
- Item one
- Item two
- Item three
## This is an h2 element
You can add an image like this:
![Alt text](https://www.pexels.com/photo/photography-of-orange-volkswagen-beetle-1209774/)
Here, we defined an MDX file with some basic markdown, including metadata.
Next, open the another-post.mdx
file and add the following to it:
---
title: "Another Blog Post"
author: "Mark Adams"
publishDate: "09/06/24"
---
# This is a another test blog post
Here is a paragraph
here is a list:
- Item one
- Item two
- Item three
## This is an h2 element
Here, we also created another Markdown file with some basic Markdown.
In the src/
directory, add a new folder called util
and create a util.ts
file in it. Open this file and add the following to it:
import fs from "fs";
import path from "path";
import { compileMDX } from "next-mdx-remote/rsc";
const rootDir = path.join(process.cwd(), "src", "blog-posts");
export const getPostBySlug = async (slug: string) => {
const formattedSlug = slug.replace(https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/2024/2024-07/\.mdx$/, "");
const filePath = path.join(rootDir, `${formattedSlug}.mdx`);
const fileContent = fs.readFileSync(filePath, { encoding: "utf8" });
const { frontmatter, content } = await compileMDX({
source: fileContent,
options: { parseFrontmatter: true },
});
return { meta: { ...frontmatter, slug: formattedSlug }, content };
};
export const getPostsMetaData = async () => {
const files = fs.readdirSync(rootDir);
let posts = [];
for (const fileName of files) {
const { meta } = await getPostBySlug(fileName);
posts.push(meta);
}
return posts;
};
In the code above, we defined utility functions to read Markdown files, compile them into HTML and extract metadata, facilitating the rendering and listing of blog posts.
The getPostBySlug
function fetches the content of a blog post by its slug (file name without the .mdx extension), constructs a file path based on the slug and reads the file content synchronously. It uses compileMDX
to compile the MDX content into HTML while extracting frontmatter (metadata) and the post content. compileMDX
is a function exposed by the next-mdx-remote
package. Finally, it returns an object containing the metadata (including the slug) and the post content.
The getPostsMetaData
function retrieves metadata for all blog posts. It reads the list of files from the blog post directory, iterates through each file to fetch its metadata using getPostBySlug
and accumulates the metadata into an array before returning it.
To render a list of blogs using data extracted from the metadata of each blog post file, create a blog
folder in the src/app/
directory. Create a page.tsx
file in this folder and add the following to it:
import React from "react";
import { getPostsMetaData } from "@/util/util";
import Link from "next/link";
type Props = {};
type PostMetaData = {
title: string;
author: string;
publishDate: string;
slug: string;
};
export default async function page({}: Props) {
const posts = (await getPostsMetaData()) as PostMetaData[];
return (
<section className="p-4">
<h1 className="text-3xl font-semibold">My Blog Posts</h1>
<div className="flex gap-4 my-8">
{posts &&
posts.map((post, index) => (
<Link
href={`/blog/${post.slug}`}
key={index}
className="p-8 border"
>
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="font-medium">{post.author}</p>
<p className="italic">{post.publishDate}</p>
</Link>
))}
</div>
</section>
);
}
Here, we imported and called the getPostsMetaData
utility function to get the metadata and render them as links on the new page. When these links are clicked, they redirect to a dynamic route that renders the blog post’s content.
Create a dynamic folder called [slug]
in the src/app/blog/
directory and add a file named page.tsx
inside it. Open page.tsx
and add the following:
import { getPostBySlug } from "@/util/util";
import React from "react";
type Props = {
params: {
slug: string;
};
};
const getPageData = async (slug: string) => {
const { meta, content } = await getPostBySlug(slug);
return { meta, content };
};
export async function generateMetadata({ params }: Props) {
const { meta } = await getPageData(params.slug);
return { title: meta.title };
}
export default async function page({ params }: Props) {
const { content } = await getPageData(params.slug);
return (
<section className="p-4">
<div className="container py-4 prose">{content}</div>
</section>
);
}
In the code above, we used the default generateMetadata
function to define the metadata since it requires dynamic values. Next, we extracted the individual post data and rendered its content to the page.
Remark and Rehype extend the capabilities of MDX by improving the parsing and processing of Markdown and JSX content. These plugins transform MDX, allowing it to achieve more.
For instance, in the demo application, we can add a plugin to parse and format code blocks within MDX files. Code blocks are defined with three backticks (```); without any plugins, they appear as plain text on the page. However, using a plugin, we can improve the appearance of these code blocks.
In this example, we will use Highlight.js to format our code blocks. Run the following command in the terminal to install Highlight.js and the other required dependencies:
npm i highlight.js rehype-highlight tailwind-highlightjs
Then, add the plugin to the the next.config.mjs
file as shown below:
import nextMDX from "@next/mdx";
import rehypeHighlight from "rehype-highlight";
const withMDX = nextMDX({
options: {
rehypePlugins: [rehypeHighlight],
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure `pageExtensions` to include MDX files
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
};
export default withMDX(nextConfig);
This already adds code block formatting; however, to style them, add one of Highlight.js themes to the Tailwind config in the tailwind.config.ts
file as shown below:
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
hljs: {
theme: "atom-one-dark",
},
},
plugins: [
require("@tailwindcss/typography"),
require("tailwind-highlightjs"),
],
safelist: [
{
pattern: /hljs+/,
},
],
};
export default config;
Let’s add a code block to one of the sample MDX files to see what we’ve got. Open the src/blog-posts/another-post.mdx
file and add a code block as shown below:
---
title: "Another Blog Post"
author: "Mark Adams"
publishDate: "09/06/24"
---
# This is a another test blog post
Here is a paragraph
here is a list:
- Item one
- Item two
- Item three
## This is an h2 element
```tsx
const test = {
name: "Jakes",
};
console.log(test.name);
You can go ahead and preview the application in the browser.
By using MDX in Next.js projects, developers can leverage Markdown’s simplicity while unlocking React components’ dynamic features. This combination enables the creation of immersive user experiences, such as interactive documentation pages and engaging blog posts.
In this article, we’ve looked at what Markdown and MDX are. We also discussed how to easily integrate Markdown with MDX in a modern Next.js application.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.