Skip to content

How to Use Next.js Image Component in Markdown

Next.js/

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> with next/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 via components prop
  • You need to first serialize your Markdown server-side in getStaticProps
  • 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:

  1. You pass ReactMarkdown an object that maps img element to a function through its components prop
  2. That function receives img element attributes that you can extract from the props argument
  3. You split the alt prop into two substrings and extract the actual alt text from first substring
  4. You extract width and heigh from the second substring using regex
  5. 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.