Nov 13, 2024• 6 min read
Next.js MDX Tutorial
Learn how to create a blog with Next.js and MDX.
Nikita Khabya
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.
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.
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.
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).
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.
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.
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}
/>
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.
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>
);
}
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.
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,
};
}
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 .