Telerik blogs

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.

Understanding Markdown

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:

  • Simplicity: Markdown reduces the learning curve for users, allowing them to focus more on content creation rather than formatting complexities.
  • Increased productivity and efficiency: Markdown simplifies the content creation workflow without the overhead of the HTML syntax.
  • Flexibility: Markdown files are plain text files that can be easily edited using any text editor and converted to various other formats.
  • Simplified collaboration: Multiple users can easily share and collaborate on Markdown. Markdown files are also compatible with version control systems like Git, which also helps with collaboration.

Markdown’s syntax includes various elements, such as:

  1. Headings: Defined by prefixing text with hash symbols (#). The number of hashes indicates the heading level, ranging from h1 to h6.
# 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. Lists: Ordered and unordered lists are defined by numbers or dashes (-), respectively, followed by a space and the list item.
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>
  1. Links: Defined using square brackets for the link text followed by parentheses for the URL.
\[Link Text\](https://www.sample-link.com)

Converts to:

<a href="https://www.sample-link.com">Link Text</a>
  1. Images: An exclamation mark (!), followed by a set of square brackets containing the image’s alt attribute text, and then a set of parentheses containing the image’s URL or path.
!\[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">

What Is MDX?

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.

Project Setup

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:

Project setup

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

Next.js project

Using MDX with Modern Next.js

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:

  • @mdx-js/loader: A webpack loader for MDX.
  • @mdx-js/react: A context-based components provider that integrates React with MDX.
  • @types/mdx: TypeScript definitions for MDX.

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.

MDX in Next.js

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.

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.

Styling MDX

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.

MDX with Tailwind plugin

The Tailwind plugin also offers some other variations of the prose class. For further details, you can explore the documentation.

Working with Remote Markdown

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.

Adding Metadata

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.

A Simple Blog

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.

Demo home page

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.

Simple blog application

Remark and Rehype Plugins

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.

Code block formatting

Conclusion

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.


About the Author

Christian Nwamba

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.

Related Posts

Comments

Comments are disabled in preview mode.