RK
Reetesh Kumar@iMBitcoinB

MDX in Next.JS with App Router Setup Guide

Jul 31, 2024

0

8 min read

MDX is a markdown format that allows you to write JSX in markdown files. You can add components, import other files, and use all the power of React in markdown files. This is a powerful feature that can be used to create dynamic content in Next.js applications.

There are so many other packages are there to use MDX in Next.JS such as using Contentlayer, Velite, MDXTS and there are many more. But in this article, we will see how to use @next/mdx in Next.JS with App Router.

Why next-mdx?#

Choosing the right package for MDX in Next.JS is very important since you want stablility and performance. @next/mdx is built by the Next.JS team and it is the official package for MDX in Next.JS. It is very stable and has good performance.

Personally for my blog, which you are reading right now is using Contentlayer for MDX in Next.JS but recently Contentlayer is not maintained and I am planning to move to @next/mdx. So i was exploring how to use @next/mdx in Next.JS with App Router and I thought to write an article on it.

How to use next-mdx in Next.JS with App Router?#

I assume you have a Next.JS application setup. Now we need to install @next/mdx package. Run the following command to install the package:

bash
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
# or
yarn add @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

These are the packages which we need to get started with @next/mdx in Next.JS. Now we need to configure the Next.JS application to use @next/mdx. Add the following code in your next.config.mjs file:

js
// next.config.mjs
import createMDX from '@next/mdx';
 
const withMDX = createMDX({
  extension: /\.mdx?$/,
});
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
};
 
export default withMDX(nextConfig);

Here we are creating a withMDX function which will be used to configure the Next.JS application to use @next/mdx. We are also adding the .mdx extension to the pageExtensions array so that Next.JS can recognize the .mdx files.

We can do all sorts of configuration with @next/mdx as it gives us option object within createMDX function. We can easily configure rehypePlugins, remarkPlugins, and many more.

now we need to add one more file which is neede for @next/mdx to work with Next.JS App Router. Create a file called mdx-components.tsx in you root directory and add the following code:

tsx
// mdx-components.tsx
 
'use client';
 
import type { MDXComponents } from 'mdx/types';
import { cn } from './lib/utils';
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    pre: ({ children, className }: any) => (
      <pre className={cn('rounded-md bg-gray-800 p-4')} {...{ className }}>
        {children}
      </pre>
    ),
    h1: ({ className, ...props }: any) => (
      <h1
        className={cn(
          'heading mt-2 scroll-m-[10px] text-4xl font-bold tracking-tight',
          className
        )}
        {...props}
      />
    ),
    h2: ({ className, ...props }: any) => (
      <h2
        className={cn(
          'heading mt-10 scroll-m-[10px] pb-1 text-[1.625rem] font-semibold tracking-tight first:mt-0',
          className
        )}
        {...props}
      />
    ),
    h3: ({ className, ...props }: any) => (
      <h3
        className={cn(
          'heading mt-8 scroll-m-[10px] text-[1.375rem] font-semibold tracking-tight',
          className
        )}
        {...props}
      />
    ),
    h4: ({ className, ...props }: any) => (
      <h4
        className={cn(
          'heading mt-8 scroll-m-[10px] text-[1.125rem] font-semibold tracking-tight',
          className
        )}
        {...props}
      />
    ),
    h5: ({ className, ...props }: any) => (
      <h5
        className={cn(
          'heading mt-8 scroll-m-[10px] text-lg font-semibold tracking-tight',
          className
        )}
        {...props}
      />
    ),
    h6: ({ className, ...props }: any) => (
      <h6
        className={cn(
          'heading mt-8 scroll-m-[10px] text-base font-semibold tracking-tight',
          className
        )}
        {...props}
      />
    ),
    p: ({ className, ...props }: any) => (
      <p
        className={cn('leading-7 [&:not(:first-child)]:mt-6', className)}
        {...props}
      />
    ),
    ul: ({ className, ...props }: any) => (
      <ul
        className={cn('my-6 ml-0 list-disc pl-0', className)}
        {...props}
        style={{ listStyle: 'none' }}
      />
    ),
    ol: ({ className, ...props }: any) => (
      <ol className={cn('my-6 ml-2 list-decimal', className)} {...props} />
    ),
    li: ({ className, ...props }: any) => (
      <li
        className={cn(
          'custom-white m-0 mt-2 flex items-start gap-2 p-0',
          className
        )}
        {...props}
      >
        <div className="w-[16px]">
          <ArrowRight size={16} color="#e21d49" style={{ marginTop: '7px' }} />
        </div>
 
        <div>{props.children}</div>
      </li>
    ),
    hr: ({ ...props }) => <hr className="my-4 md:my-8" {...props} />,
    table: ({
      className,
      ...props
    }: React.HTMLAttributes<HTMLTableElement>) => (
      <div className="my-6 w-full overflow-y-auto">
        <table className={cn('w-full', className)} {...props} />
      </div>
    ),
    tr: ({
      className,
      ...props
    }: React.HTMLAttributes<HTMLTableRowElement>) => (
      <tr
        className={cn('even:bg-muted m-0 border-t p-0', className)}
        {...props}
      />
    ),
    th: ({ className, ...props }: any) => (
      <th
        className={cn(
          'border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
          className
        )}
        {...props}
      />
    ),
    td: ({ className, ...props }: any) => (
      <td
        className={cn(
          'border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
          className
        )}
        {...props}
      />
    ),
 
    code: ({ className, ...props }: any) => (
      <code
        className={cn(
          'relative px-[0.3rem] py-[0.2rem] font-mono text-sm',
          className
        )}
        {...props}
      />
    ),
    ...components,
  };
}

Above code is a pretty simple code which is used to style the markdown content. You can add your own styles to the markdown content by modifying the above code. Here we are using TailwindCSS for styling the markdown content but feel free to use any other CSS framework or your own custom CSS.

You can even add your own components to the useMDXComponents function to use them in the markdown content. This is a very powerful feature of @next/mdx which allows you to create dynamic content in Next.JS applications.

With this setup, We are ready to use @next/mdx in Next.JS with App Router. You can now write markdown content in your application and use the power of React to create dynamic content.

Partial Prerendering in Next.js explained

Partial Prerendering in Next.js caches prerendered parts of a page to serve them as they are ready. This experimental feature is not yet supported by all deployment platforms.

Read Full Post
Partial Prerendering in Next.js explained

Writing MDX content in Next.JS#

You can now create a directory hello in your app directory and create a file called page.tsx and hello.mdx in it. Add the following code to the hello.mdx file:

mdx
// app/hello/hello.mdx
 
# Hello World
 
This is a **Hello World** example using **@next/mdx** in Next.JS with App Router.

Now you can import the hello.mdx file in the page.tsx file and render it there.

tsx
// app/hello/page.tsx
 
'use client';
 
import React from 'react';
import Hello from './hello.mdx';
 
const HomePage = () => {
  return <Hello />;
};
 
export default HomePage;

Now you can run your Next.JS application and navigate to the /hello route to see the markdown content rendered on the page.

This is bare minimum setup to when we talk about actual project like writing a tech blog we want our blog post to have everyting cover like highlighting the code, adding images, adding tables, adding links and many more. So we need to add more configuration to the mdx-components.tsx file to make it more powerful.

Integrating Rehype and Remark plugins#

@next/mdx allows you to use rehypePlugins and remarkPlugins to customize the markdown content. You can add plugins to the createMDX function to use them in your Next.JS application.

bash
yarn add rehype-pretty-code rehype-autolink-headings rehype-slug remark-gfm shikiji -D
js
// next.config.mjs
 
import createMDX from '@next/mdx';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
 
const withMDX = createMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [
      rehypeSlug,
      [
        rehypePrettyCode,
        {
          theme: 'andromeeda',
        },
      ],
      [rehypeAutolinkHeadings],
    ],
  },
});
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
};
 
export default withMDX(nextConfig);

here we are adding remarkGfm plugin to the remarkPlugins array to enable GitHub Flavored Markdown in the markdown content. We are also adding rehypeSlug, rehypePrettyCode, and rehypeAutolinkHeadings plugins to the rehypePlugins array to customize the markdown content.

You can read more about the plugins in the rehype-pretty-code, rehype-autolink-headings, rehype-slug and remark-gfm documentation.

With above setup your code block will be highlighted, headings will be linked and slugified and many more.

Custom components in MDX#

The good thing about @next/mdx is that you can use custom components in the markdown content. You can create custom components and provide to the useMDXComponents function to use them in the markdown content.

tsx
// Callout.tsx
 
type TCallout = {
  emoji?: string;
  children: React.ReactNode;
  type?: 'default' | 'warning' | 'danger';
  className?: string;
};
 
const Callout: React.FC<TCallout> = ({
  emoji,
  children,
  type = 'default',
  className,
}) => {
  return (
    <div
      className={cn('my-6 flex items-start rounded-md border border-l-4 p-4', {
        'border-red-900 bg-red-50': type === 'danger',
        'border-yellow-900 bg-yellow-50': type === 'warning',
        className,
      })}
    >
      {emoji && <span className="mr-4 text-2xl">{emoji}</span>}
      <div>{children}</div>
    </div>
  );
};
 
export default Callout;

We just created a Callout component which can be used in the markdown content. You can now provide this component to the useMDXComponents function to use it in the markdown content.

tsx
'use client';
 
import type { MDXComponents } from 'mdx/types';
import { cn } from './lib/utils';
import Callout from './callout';
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // ..
    Callout,
    ...components,
  };
}

Now you can use the Callout component in the markdown content like this:

mdx
<Callout emoji="💡">Hello from the Callout component!</Callout>

This is how you can use custom components in the markdown content using @next/mdx in Next.JS with App Router.

Conclusion#

@next/mdx is a good alternative to other MDX packages for Next.JS applications in my little experience. It is very stable and has good performance and fact that backed by Next.JS team makes it more reliable.

We get a lot of flexibility with @next/mdx to customize the markdown content and use custom components in the markdown content. We can also use rehypePlugins and remarkPlugins to customize the markdown content further.

I hope this article helps you to get started with @next/mdx in Next.JS with App Router. If you have any questions or feedback, feel free to leave a comment below. Happy coding! 🚀

Comments (0)

Related Posts

Made with ❤️ by Reetesh