Nov 13, 2024 6 min read 

Next.js MDX Tutorial

Learn how to create a blog with Next.js and MDX.

Nikita Khabya

Nikita Khabya

Next.js MDX Tutorial

Next.js is a React framework that allows you to build static and server-rendered websites. MDX is a file format that enables you to write JSX in Markdown files. This tutorial guides you through creating a blog with Next.js and MDX.

Introduction to Next.js and MDX

Next.js enables you to build performant web applications with static or server-side rendering. MDX, meanwhile, lets you embed JSX directly within Markdown, making it perfect for writing content-rich pages with custom components.

Step 1: Set Up Your Next.js App

Start by creating a new Next.js app, or use an existing one. If creating a new app, run:

npx create-next-app@latest

You now have a fresh next.js app with all the necessary files and folders. Getting ready is basically setting up linting, formatting, or any other of your go to tools.

Step 2: Configure MDX Support in Next.js

Next.js supports usage of markdown but it needs a little setup from our end

Next.js can support both local MDX content inside your application, as well as remote MDX files fetched dynamically on the server. The Next.js plugin handles transforming markdown and React components into HTML, including support for usage in Server Components (the default in App Router).

Next.js Plugin for MDX

npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
import createMDX from '@next/mdx';

const nextConfig = {
	// Configure `pageExtensions` to include markdown and MDX files
	pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
	// Optionally, add any other Next.js config below
};

const withMDX = createMDX({
	// Add markdown plugins here, as desired
});

// Merge MDX config with Next.js config
export default withMDX(nextConfig);

Add mdx-components.tsx file.
Create an mdx-components.tsx (or .js) file in the root of your project to define global MDX Components. For example, at the same level as pages or app, or inside src if applicable.

import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
	return {
		...components,
	};
}

The configuration is done, now we can start writing our blog posts in mdx format.

Step 3: Rendering MDX with Next MDX Remote

One of the way to source MDX content in Next.js is by using the Next MDX Remote package. This tool is designed for scenarios involving “remote” content—either outside the app directory or truly remote, like content fetched from a server. You can use Next MDX Remote to render your content directly from its existing location.

Read more: Next.js MDX Remote

npm install next-mdx-remote

So for this project, majorly I've 2 routes

  • blogs --> to list all the blogs

  • blog/[slug] --> to render a specific blog


    I'll be using the nextjs dynamic routing to render the individual blog page.

    • To render the blogs and the blog details page, we're going to use the Next MDX Remote package along with fs and path modules to read the content from the file system.

1. Install dependencies

import { MDXRemote } from 'next-mdx-remote/rsc';
import { promises as fs } from 'fs';
import path from 'path';

2. Extract slug from the params

export default function BlogDetailPage({
	params,
}: {
	params: { slug: string };
}) {
	return <div>{params.slug}</div>;
}

3. Read the content from the file system

const content = await fs.readFile(
	path.join(process.cwd(), 'src/blogs', `${params.slug}.mdx`),
	'utf-8'
);

Here we're reading the content from the file system. We use path.join to construct the path to the file, and then read the content using fs.readFile. process.cwd() returns the current working directory, which is the root directory of the project.

4. Render the content using MDXRemote

import { MDXRemote } from 'next-mdx-remote/rsc';
import { promises as fs } from 'fs';
import path from 'path';

export default async function BlogDetailPage({
	params,
}: {
	params: { slug: string };
}) {
	const content = await fs.readFile(
		path.join(process.cwd(), 'src/blogs', `${params.slug}.mdx`),
		'utf-8'
	);
	return (
		<div>
			<MDXRemote source={content} />
		</div>
	);
}

Now if you've a simple markdown syntax then you should be able to render the content.


P.S: Refresh the page and you'll see the content is rendered.

But if you're trying to make a pretty blog then you can use your components and styles to render the content.

5. Custom components

To be able to use your components and styles, you need to import them in the MDXRemote component.

export default function BlogHeader({
	className,
	children,
}: {
	className?: string;
	children: React.ReactNode;
}) {
	return (
		<h2 className={`text-2xl font-bold text-indigo-500 ${className}`}>
			{children}
		</h2>
	);
}
<MDXRemote
	components={{
		BlogHeader,
	}}
	source={content}
/>
STEP 4: Parsing Frontmatter in MDX

Frontmatter is metadata that is placed at the top of a file, between the opening and closing triple dashes (---). It is used to provide additional information about the file, such as the title, author, and date.

  • To parse frontmatter in MDX, here we’re going to use a different function from the Next Remote MDX package.

1. Update the import statement

import { compileMDX } from 'next-mdx-remote/rsc';

2. Function to get frontmatter and content

interface Frontmatter {
  title: string;
}

const data = await compileMDX<Frontmatter>({
  source: content,
  options: {
    parseFrontmatter: true
  },
  components: {
    // Your Components here
  }
})

3. Use the data

This way you can render frontmatter and content of your blog

import { compileMDX } from 'next-mdx-remote/rsc';
import { promises as fs } from 'fs';
import path from 'path';
import BlogHeader from '../../../components/BlogHeader';

interface Frontmatter {
	title: string;
	author: string;
	description: string;
	publishedAt: string;
}

export default async function BlogDetailPage({
	params,
}: {
	params: { slug: string };
}) {
	const content = await fs.readFile(
		path.join(process.cwd(), 'src/blogs', `${params.slug}.mdx`),
		'utf-8'
	);

	const data = await compileMDX<Frontmatter>({
		source: content,
		options: {
			parseFrontmatter: true,
		},
		components: {
			BlogHeader,
		},
	});

	return (
		<div>
			<h1>Title: {data.frontmatter.title}</h1>
			<p>Author: {data.frontmatter.author}</p>
			<div>Content: {data.content}</div>
		</div>
	);
}
STEP 5: Displaying the list of blogs

We'll achieve this by reading all the files from the blogs directory and display them on the blogs page.

1. Import dependencies

import { compileMDX } from 'next-mdx-remote/rsc';
import { promises as fs } from 'fs';
import path from 'path';

2. Read all the files from the blogs directory

const blogFiles = await fs.readdir(path.join(process.cwd(),'src/blogs')); 

3. Creating a list of blogs

interface Frontmatter {
  title: string;
}

const blogs = await Promise.all(blogFiles.map(async (filename) => {
const content = await fs.readFile(path.join(process.cwd(), 'src/blogs', filename), 'utf-8');

const { frontmatter } = await compileMDX<Frontmatter>({
	source: content,
	options: {
	parseFrontmatter: true
	}
})

return {
	filename,
	slug: filename.replace('.mdx', ''),
	...frontmatter
	}
}))

4. Displaying the list of blogs + Entire code

import { promises as fs } from 'fs';
import { compileMDX } from 'next-mdx-remote/rsc';
import Link from 'next/link';
import path from 'path';

interface Frontmatter {
title: string;
}

export default async function BlogPage() {
	const blogFiles = await fs.readdir(path.join(process.cwd(), 'src/blogs'));

    const blogs = await Promise.all(
    	blogFiles.map(async (filename) => {
    		const content = await fs.readFile(
    			path.join(process.cwd(), 'src/blogs', filename),
    			'utf-8'
    		);
    		const { frontmatter } = await compileMDX<Frontmatter>({
    			source: content,
    			options: {
    				parseFrontmatter: true,
    			},
    		});
    		return {
    			filename,
    			slug: filename.replace('.mdx', ''),
    			...frontmatter,
    		};
    	})
    );

    return (
    	<div className='grid grid-cols-2 gap-10'>
    		{blogs.map((blog) => (
    			<Link href={`/blogs/${blog.slug}`} key={blog.slug} className=''>
    				{blog.title}
    			</Link>
    		))}
    	</div>
    );
}

Finally, with all the code in place, we can now run the application and see the list of blogs.

STEP 6: SEO for the blog

1. SEO of /blogs route

Here, we can simply write the getMetadata function from the Next.js and it will work just fine.

export const metadata: Metadata = {
	title: 'All Blogs',
	description: 'A collection of all the blogs.',
};

2. SEO of /blogs/[slug] route

Here, we need to make just slight bit more efforts as follows

export async function generateMetadata({
		params,
		}: {
			params: { blogSlug: string };
		}) {
			const content = await fs.readFile(
				path.join(process.cwd(), 'src/blogs', `${params.blogSlug}.mdx`),
				'utf-8'
				);
			
			const { frontmatter } = await compileMDX<{ title: string }>({
				source: content,
				options: {
				parseFrontmatter: true,
				},
			});

		return {
			title: frontmatter.title,
		};
}
Conclusion

This way we can generate the metadata for the individual blog pages.

I know the demo is not that fancy but the blog that you're reading now is also built using the same logic and also a little more efforts :P

Tags

nextjs

mdx

react

As always if you have any questions or need more info, feel free to ping me .

January 22, 2025 at 07:23 PM

Mumbai, India


Thanks for visiting, have a great day!

© 2025 Nikita Khabya