MDX Blog with Next.js 13
Published
· 2 years ago
next.jsmdxblogtypescriptreactmarkdown
Next.js version 13 introduced the
In this article, we'll go over the basics of using next-mdx-remote to render MDX files with Next.js 13 and TypeScript.
The code we'll be writing in this article is available on GitHub.
/app
directory, a new way of defining routes in your application.In this article, we'll go over the basics of using next-mdx-remote to render MDX files with Next.js 13 and TypeScript.
The code we'll be writing in this article is available on GitHub.
Update
I now recommend using Contentlayer instead of next-mdx-remote.
It takes much less code to set up and it works beautifully with Next.js 13.
It takes much less code to set up and it works beautifully with Next.js 13.
Setup
First, we'll need to create a new Next.js 13 app:
npx create-next-app@latest --experimental-app
# or
yarn create next-app --experimental-app
# or
pnpm create next-app --experimental-app
npx create-next-app@latest --experimental-app
# or
yarn create next-app --experimental-app
# or
pnpm create next-app --experimental-app
Next, we'll need to install next-mdx-remote:
npm install next-mdx-remote
# or
yarn add next-mdx-remote
# or
pnpm add next-mdx-remote
npm install next-mdx-remote
# or
yarn add next-mdx-remote
# or
pnpm add next-mdx-remote
Creating a Post
Let's create an example post.
Create a new directory
/content
in the project root and create a new file /content/post.mdx
:/content/post.mdx
---
title: Example Post
date: '2022-12-11'
---
This is an example post.
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
<Card>
### This Card is a **custom component**.
Markdown **can** be used *inside* custom components.
</Card>
/content/post.mdx
---
title: Example Post
date: '2022-12-11'
---
This is an example post.
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
<Card>
### This Card is a **custom component**.
Markdown **can** be used *inside* custom components.
</Card>
Note that we're using a custom component called
<Card>
<Card>
in the post. We'll define this component later on.Defining Types
Let's head over to
We'll define 2 types,
/app/page.tsx
, the home page of our app, and where we'll be rendering our post.We'll define 2 types,
Frontmatter
Frontmatter
and Post
Post
:Frontmatter
Frontmatter
will define which properties we want to extract from the frontmatter of our MDX files. In this case, we'll extract thetitle
title
anddate
date
.Post
Post
will define the shape of our post. It will contain the serialized MDX content and the frontmatter.
/app/page.tsx
import { type MDXRemoteSerializeResult } from 'next-mdx-remote';
type Frontmatter = {
title: string;
date: string;
};
type Post<TFrontmatter> = {
serialized: MDXRemoteSerializeResult;
frontmatter: TFrontmatter;
};
/app/page.tsx
import { type MDXRemoteSerializeResult } from 'next-mdx-remote';
type Frontmatter = {
title: string;
date: string;
};
type Post<TFrontmatter> = {
serialized: MDXRemoteSerializeResult;
frontmatter: TFrontmatter;
};
Serializing the Post
Next, we'll define an
async function
async function
called getPost
getPost
that takes a filepath, and returns a Post
Post
object by reading the file from the filesystem and serializing the MDX content./app/page.tsx
// ...
import { promises as fs } from 'fs';
import { serialize } from 'next-mdx-remote/serialize';
// ...
async function getPost(filepath: string): Promise<Post<Frontmatter>> {
// Read the file from the filesystem
const raw = await fs.readFile(filepath, 'utf-8');
// Serialize the MDX content and parse the frontmatter
const serialized = await serialize(raw, {
parseFrontmatter: true,
});
// Typecast the frontmatter to the correct type
const frontmatter = serialized.frontmatter as Frontmatter;
// Return the serialized content and frontmatter
return {
frontmatter,
serialized,
};
}
/app/page.tsx
// ...
import { promises as fs } from 'fs';
import { serialize } from 'next-mdx-remote/serialize';
// ...
async function getPost(filepath: string): Promise<Post<Frontmatter>> {
// Read the file from the filesystem
const raw = await fs.readFile(filepath, 'utf-8');
// Serialize the MDX content and parse the frontmatter
const serialized = await serialize(raw, {
parseFrontmatter: true,
});
// Typecast the frontmatter to the correct type
const frontmatter = serialized.frontmatter as Frontmatter;
// Return the serialized content and frontmatter
return {
frontmatter,
serialized,
};
}
Getting the Post Inside a Page Component
Now let's get this post inside our
<Home>
<Home>
component. We'll have to change the <Home>
<Home>
component to be an async
async
server component so we can use await
await
in the top level of the component./app/page.tsx
export default async function Home() {
// Get the serialized content and frontmatter
const { serialized, frontmatter } = await getPost('content/post.mdx');
return (
<div style={{ maxWidth: 600, margin: 'auto' }}>
<h1>{frontmatter.title}</h1>
<p>Published {frontmatter.date}</p>
</div>
);
}
/app/page.tsx
export default async function Home() {
// Get the serialized content and frontmatter
const { serialized, frontmatter } = await getPost('content/post.mdx');
return (
<div style={{ maxWidth: 600, margin: 'auto' }}>
<h1>{frontmatter.title}</h1>
<p>Published {frontmatter.date}</p>
</div>
);
}
We should now see the title and date of our post rendered on the page.
Rendering the Post's Content
next-mdx-remote provides a component called
<MDXRemote>
<MDXRemote>
, that is used to render MDX content. To use it, we must wrap it in a client component.Client Components are rendered on the client. With Next.js, Client Components can also be pre-rendered on the server and hydrated on the client.Today, many components from npm packages that use client-only features do not yet have the directive. These third-party components will work as expected within your own Client Components, since they themselves have the "use client" directive, but they won't work within Server Components. source
Let's create a new component file called
/app/mdx-content.tsx
:/app/mdx-content.tsx
'use client';
import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote';
type MdxContentProps = {
source: MDXRemoteSerializeResult;
};
export function MdxContent({ source }: MdxContentProps) {
return <MDXRemote {...source} />;
}
/app/mdx-content.tsx
'use client';
import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote';
type MdxContentProps = {
source: MDXRemoteSerializeResult;
};
export function MdxContent({ source }: MdxContentProps) {
return <MDXRemote {...source} />;
}
All this component does is render the
<MDXRemote>
<MDXRemote>
component.If we want to add custom components to our posts, we can do so by passing a
components
components
prop:/app/mdx-content.tsx
// ...
/** Place your custom MDX components here */
const MdxComponents = {
/** h1 colored in yellow */
h1: (props: React.HTMLProps<HTMLHeadingElement>) => (
<h1 style={{ color: '#FFF676' }} {...props} />
),
/** Card component */
Card: (props: React.HTMLProps<HTMLDivElement>) => (
<div
style={{
background: '#333',
borderRadius: '0.25rem',
padding: '0.5rem 1rem',
}}
{...props}
/>
),
};
export function MdxContent({ source }: MdxContentProps) {
return <MDXRemote {...source} components={MdxComponents} />;
}
/app/mdx-content.tsx
// ...
/** Place your custom MDX components here */
const MdxComponents = {
/** h1 colored in yellow */
h1: (props: React.HTMLProps<HTMLHeadingElement>) => (
<h1 style={{ color: '#FFF676' }} {...props} />
),
/** Card component */
Card: (props: React.HTMLProps<HTMLDivElement>) => (
<div
style={{
background: '#333',
borderRadius: '0.25rem',
padding: '0.5rem 1rem',
}}
{...props}
/>
),
};
export function MdxContent({ source }: MdxContentProps) {
return <MDXRemote {...source} components={MdxComponents} />;
}
Now let's import the
<MdxContent>
<MdxContent>
component into our <Home>
<Home>
component and render it, passing in the serialized content from our post:/app/page.tsx
// ...
import { MdxContent } from './mdx-content';
// ...
export default async function Home() {
// Get the serialized content and frontmatter
const { serialized, frontmatter } = await getPost('content/post.mdx');
return (
<div style={{ maxWidth: 600, margin: 'auto' }}>
<h1>{frontmatter.title}</h1>
<p>Published {frontmatter.date}</p>
<hr />
<MdxContent source={serialized} />
</div>
);
}
/app/page.tsx
// ...
import { MdxContent } from './mdx-content';
// ...
export default async function Home() {
// Get the serialized content and frontmatter
const { serialized, frontmatter } = await getPost('content/post.mdx');
return (
<div style={{ maxWidth: 600, margin: 'auto' }}>
<h1>{frontmatter.title}</h1>
<p>Published {frontmatter.date}</p>
<hr />
<MdxContent source={serialized} />
</div>
);
}
That's it! We've successfully rendered an MDX file on the page using next-mdx-remote, and we've added custom components to it.
Conclusion and Next Steps
In this article, we've only scratched the surface of using next-mdx-remote with Next.js 13.
To make an actual blog, we might want to move our
To make an actual blog, we might want to move our
getPost
getPost
function to a file called /lib/mdx.ts
(or similar) and add two more functions: getAllPosts
getAllPosts
, which returns all of the posts in our blog, and getAllMdxFiles
getAllMdxFiles
, which returns all of the MDX files in the content directory.We can then leverage Next.js 13's Dynamic Segments and Static Params Generation to create a dynamic route for each post, and render the post's content on the page.
Update
My blog now uses Contentlayer instead of next-mdx-remote to render MDX files.