Skip to main content
views/

Use Next/Image with React Markdown

Amir Ardalan

In a previous post, I explained how to add syntax highlighting and individual line highlighting to react-markdown using react-syntax-highlighter and parse-numeric-range.

Custom components are a powerful way to extend react-markdown. In this guide, we will create a component that converts Markdown images into Next.js Image components.

This gives us the concise syntax of Markdown along with the optimization and configuration options provided by next/image. As a bonus, we will also add support for image captions directly within the Markdown.

Create Markdown.tsx

// Markdown.tsx
import Image from 'next/image'
import ReactMarkdown from 'react-markdown'

const MarkdownComponents: object = {
  // Code will go here
}

return (
  <ReactMarkdown
    children={your.content.here}
    components={MarkdownComponents}
  />
)
tsx

Add Custom Metastring Logic

For anyone who wants to get straight to the implementation, here is the final code. The rest of the post explains how it works.

p: (paragraph: { children?: boolean; node?: any }) => {
  const { node } = paragraph

  if (node.children[0].tagName === 'img') {
    const image = node.children[0]
    const metastring = image.properties.alt
    const alt = metastring?.replace(/ *\{[^)]*\} */g, '')
    const metaWidth = metastring?.match(/{([^}]+)x/)
    const metaHeight = metastring?.match(/x([^}]+)}/)
    const width = metaWidth ? metaWidth[1] : '768'
    const height = metaHeight ? metaHeight[1] : '432'
    const isPriority =
      metastring?.toLowerCase().includes('{priority}') ?? false
    const hasCaption =
      metastring?.toLowerCase().includes('{caption:') ?? false
    const caption = metastring?.match(/{caption: (.*?)}/)?.pop()

    return (
      <div className="postImgWrapper">
        <Image
          src={image.properties.src}
          width={width}
          height={height}
          className="postImg"
          alt={alt}
          priority={isPriority}
        />
        {hasCaption ? (
          <div className="caption" aria-label={caption}>
            {caption}
          </div>
        ) : null}
      </div>
    )
  }

  return <p>{paragraph.children}</p>
},
tsx

With this code, we can use the standard Markdown image syntax:

![Alt text](/image.jpg)
markdown

The image will then be rendered using the Next.js Image component.

We can also include metadata within the alt text to define the image's width and height.

We can also preload images that appear above the fold by applying the priority prop. All metadata contained within curly braces will be removed from the rendered alt text.

Here is how we would define a width of 768, a height of 432, and mark the image as a priority:

![Alt text {priority}{768x432}](/image.jpg)
markdown

To add a caption, we can use the following syntax:

![Alt text {768x432}{priority}{caption: Photo by Someone}](/image.jpg)
markdown

How Does It Work?

The implementation primarily relies on react-markdown's built-in component overrides, along with a few regular expressions.

Let's break it down.

The Logical Starting Point

const MarkdownComponents: object = {
  img: image => {
    return (
      <Image
        src={image.properties.src}
        alt={image.properties.alt}
        height="768"
        width="432"
      />
    )
  },
}
tsx

The code above should work, and to an extent, it does. It replaces a standard Markdown image with a Next.js Image component.

However, the image remains wrapped in the paragraph element generated by Markdown. This is not necessarily a major problem, but removing the wrapper gives us cleaner markup and more control over the layout.

Fixing the Wrapping Paragraph Issue

Markdown images are wrapped in paragraph elements by default. We can override the paragraph renderer, detect when its first child is an image, and return the image component directly.

p: paragraph => {
const { node } = paragraph
if (node.children[0].tagName === 'img') {
const image = node.children[0]
return (
<Image
src={image.properties.src}
width="768"
height="432"
alt={image.properties.alt}
/>
)
}
return <p>{paragraph.children}</p>
},
tsx

This is cleaner, but the image dimensions are still hardcoded. It would also be useful to support Next.js's priority prop so that we can preload an image that appears above the fold.

Setting the Next/Image Dimensions

p: paragraph => {
const { node } = paragraph
if (node.children[0].tagName === 'img') {
const image = node.children[0]
const metastring = image.properties.alt
const alt = metastring?.replace(/ *\{[^)]*\} */g, '')
const metaWidth = metastring?.match(/{([^}]+)x/)
const metaHeight = metastring?.match(/x([^}]+)}/)
const width = metaWidth ? metaWidth[1] : '768'
const height = metaHeight ? metaHeight[1] : '432'
return (
<Image
src={image.properties.src}
width={width}
height={height}
alt={alt}
/>
)
}
return <p>{paragraph.children}</p>
},
tsx

First, we declare metaWidth and metaHeight. These expressions look for dimensions enclosed in curly braces and return the values on either side of the x, as in {768x432}.

We then declare width and height, using the extracted values when they are available and falling back to 768 and 432 when dimensions have not been provided.

Finally, we create the alt value by removing the metadata enclosed in curly braces from the original alt text.

Taking Advantage of the priority Prop

We can also support Next.js's priority prop so that selected above-the-fold images are preloaded.

const MarkdownComponents: object = {
p: paragraph => {
const { node } = paragraph
if (node.children[0].tagName === 'img') {
const image = node.children[0]
const metastring = image.properties.alt
const alt = metastring?.replace(/ *\{[^)]*\} */g, '')
const metaWidth = metastring?.match(/{([^}]+)x/)
const metaHeight = metastring?.match(/x([^}]+)}/)
const width = metaWidth ? metaWidth[1] : '768'
const height = metaHeight ? metaHeight[1] : '432'
const isPriority =
metastring?.toLowerCase().includes('{priority}') ?? false
return (
<Image
src={image.properties.src}
width={width}
height={height}
className="postImg"
alt={alt}
priority={isPriority}
/>
)
}
return <p>{paragraph.children}</p>
},
}
tsx

Here, we check the metadata for {priority} and pass the resulting Boolean value to the priority prop.

Adding an Image Caption

As an additional feature, we can add image captions directly from the Markdown.

To do this, we add another regular expression that looks for a {caption:} metadata string. Anything after the colon will be displayed as the image caption.

We also wrap the image and caption in a div, allowing them to be styled and positioned together.

![Alt text {768x432}{priority}{caption: Photo by Someone}](/image.jpg)
markdown

The complete code looks like this:

p: (paragraph: { children?: boolean; node?: any }) => {
const { node } = paragraph
if (node.children[0].tagName === 'img') {
const image = node.children[0]
const metastring = image.properties.alt
const alt = metastring?.replace(/ *\{[^)]*\} */g, '')
const metaWidth = metastring?.match(/{([^}]+)x/)
const metaHeight = metastring?.match(/x([^}]+)}/)
const width = metaWidth ? metaWidth[1] : '768'
const height = metaHeight ? metaHeight[1] : '432'
const isPriority =
metastring?.toLowerCase().includes('{priority}') ?? false
const hasCaption =
metastring?.toLowerCase().includes('{caption:') ?? false
const caption = metastring?.match(/{caption: (.*?)}/)?.pop()
return (
<div className="postImgWrapper">
<Image
src={image.properties.src}
width={width}
height={height}
className="postImg"
alt={alt}
priority={isPriority}
/>
{hasCaption ? (
<div className="caption" aria-label={caption}>
{caption}
</div>
) : null}
</div>
)
}
return <p>{paragraph.children}</p>
},
tsx

A regular-expression lookbehind could provide a cleaner way to extract the caption. However, Safari did not support lookbehind expressions when this post was written, so this implementation uses the following workaround:

const caption = metastring?.match(/{caption: (.*?)}/)?.pop()
javascript

Final Thoughts

react-markdown's custom component system is a powerful way to extend Markdown rendering.

I chose to use a Gatsby-style metadata syntax here, but the same underlying approach could be adapted to support other image properties or custom Markdown behavior.