How to Use Next.js Image Component in Markdown
If you know about the benefits of using the Image component provided by Next.js, it is tempting to use it everywhere.
But, the images coming from Markdown are plain old <img>
elements. So, you might be wondering how to use next/image
with Markdown instead.
There is more than one solution to this problem. And in this post, we will look into two possible 2 ways:
- Using
next/image
inside Markdown with MDX - Substituting
<img>
withnext/image
with react-markdown
You could also potentially change <img>
elements to next/image
by hand using unified ecosystem, but that’s out of scope of this article.
Now, let’s jump right into this challenge.
Using MDX
The easiest way to use next/image
in Markdown is to use MDX, since it allows you to use JSX inside Markdown. In other words, you can use React components inside Markdown with MDX.
First, you need to add MDX support to your project.
You can check out my article about adding MDX to a Next.js project to learn how to do that. Another option is to use Contentlayer, which already has built-in MDX support.
Let’s use next-mdx-remote to accomplish this. Go ahead and install it as dependency.
npm i next-mdx-remote
Usually you keep MDX content in a file with a .mdx
extension. Then you can use Node’s File System module to read it.
For the sake of simplicity, I am storing the Markdown in a variable here.
import Image from 'next/image';
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote } from 'next-mdx-remote';
const components = { Image };
export async function getStaticProps() {
const markdown = `# Hello word
This is a placeholder text
<Image src="/image.jpg" alt="alt text" width={600} height={300} />
`;
const result = await serialize(markdown);
return { props: { content: result } };
}
export default function Post({ content }) {
return <MDXRemote {...content} components={components} />;
}
As you can see, you can use Image
component inside your Markdown.
A couple of important things to notice:
- You need to pass
MDXRemote
the components you use in your Markdown viacomponents
prop - You need to first
serialize
your Markdown server-side ingetStaticProps
- You can render the compiled result with
MDXRemote
component
If you are using frontmatter in your Markdown, you can tell next-mdx-remote
to parse it.
const result = await serialize(markdown, { parseFrontmatter: true });
console.log(result.frontmatter);
As shown, with MDX, you can use next/image
directly in Markdown. But, you can also wrap it in your own custom component and use that instead.
import Image from 'next/image';
export default function CustomImage({ src, alt, width, height, alt }) {
return <Image src={src} alt={alt} width={width} height={height} />
}
CustomImage.defaultProps = {
alt: '',
width: 800,
height: 600,
};
The benefit of doing it this way, is having fallback values via defaultProps
.
Another reason is having to adjust only one component if you no longer want to use next/image
. Just swap Image
with <img>
and that’s it, you are back to using plain old image element.
Substituting the Image Element
If you want to stick to using pure Markdown but still use the Next.js Image component, another approach is to change the <img>
element to next/image
during the process of turning Markdown into HTML.
You can use react-markdown to change <img>
any way you want.
You can also use MDX libraries such as next-mdx-remote and mdx-bundler to automatically change <img>
to next/image
.
The easiest way is to use react-markdown
, so let’s stick to that.
Using react-markdown
The goal is to substitute the <img>
element with next/image
.
The first thing you should do is to specify image dimensions inside Markdown, because next/image
needs them. You can use the space for alt text to insert this information about the image.
In this case, I will use the following format {{ w: 600, h: 300 }}
to specify image dimensions:
![alt text {{ w: 600, h: 300 }}](/image.jpg)
You can come up with your own. But, you will need to find a way to extract it from the rest of the alt text.
And here’s how you can use component substitution in react-markdown to swap out the basic <img>
element with the Next.js Image
component.
import Image from 'next/image';
import ReactMarkdown from 'react-markdown';
export default function Post() {
const markdown = `# Hello word
This is a paragraph
![alt text {{ w: 600, h: 300 }}](/image.jpg)
`;
return (
<ReactMarkdown
components={{
img: function ({ ...props }) {
const substrings = props.alt?.split('{{');
const alt = substrings[0].trim();
const width = substrings[1] ? substrings[1].match(/(?<=w:\s?)\d+/g)[0] : 800;
const height = substrings[1] ? substrings[1].match(/(?<=h:\s?)\d+/g)[0] : 400;
return <Image src={props.src} alt={alt} width={width} height={height} />;
},
}}
>
{markdown}
</ReactMarkdown>
);
}
Here’s what’s happening in this code:
- You pass
ReactMarkdown
an object that mapsimg
element to a function through itscomponents
prop - That function receives
img
element attributes that you can extract from theprops
argument - You split the alt prop into two
substrings
and extract the actualalt
text from first substring - You extract
width
andheigh
from the second substring using regex - Finally, you set fallback values in case the alt text in Markdown doesn’t contain required properties
Speaking about regex, in JavaScript, it goes between /
and /g
. Although regex can look scary, the (?<=w:\s?)\d+
expression can be explained in 2 parts.
The expression only matches digits (\d+
) inside a given string, but with a positive lookbehind. The lookbehind checks that the string contains a w:
substring before the digits.
In this case, the w:
and digits are separated with a whitespace, but it can be left out (w:600
). So you check for an optional whitespace with \s?
.
The other expression used for height
— (?<=h:\s?)\d+
— does the same, except we are looking for an h
instead of w
.
You can use RegExr to help build and test your own regex. It explains every token in your expression and speeds up the learning process.