#code

7 min read

views

Use Next/Image with React Markdown

By Amir Ardalan
Important

This post is quite old and no longer being updated. The following information may no longer work the with the latest version of the tools, libraries, frameworks, or best-practices discussed.

In a previous post, I discussed how to get code syntax highlighting and individual line highlighting working in 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 custom component to that converts Markdown images into Next.js Image components. We'll get the best of both worlds with the terse syntax of Markdown and the powerful optimization and configuration of the next/image component. As a bonus, we will add the ability to add an image caption directly in the markdown as well.

#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

If you want to get right down to business, here is the final code. Read on to see how it all 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().match('{priority}')
    const hasCaption = metastring?.toLowerCase().includes('{caption:')
    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 the above code we can use the standard Markdown style image syntax: ![AltTextHere](/image.jpg) and we will get a Next Image component rendering the image.

We can also use JSON metastrings inside the Alt text to define a width and height.

Note

As of Next.js 11, next/image no longer requires height and width to be explicitly defined for local images, but for this to work, the image's src must be declared using the import keyword, which wont work for us since we aren't hardcoding a specific image.

Finally, we can optionally preload images which will render above the fold by applying the Next/Image prop priority. All meta info contained within curly braces will be stripped before rendering.

Here's how we'd define a width of 786, height of 432 and flag as a priority image:

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

To add a caption to our image, we can do something like:

![AltText {768x432} {priority} {caption: Photo by Someone}](/image.jpg)
markdown
#So How Does it Work?

It's mostly react-markdown's built-in component overrides with some Regex magic sprinkled in. 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 above code should work, and it sort of does. It replaces a standard Markdown image with a fancy Next Image component. But if you pop open the console, there is an error. The image is wrapped in a paragraph tag. Not a major issue, but we can make it cleaner.

#Fixing the Wrapping Paragraph Issue

By default in Markdown, images are wrapped with a paragraph tag. While this isn't the worst thing in the world, it's technically not valid HTML. Let's fix that:

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

Much better, but, the image dimensions are hardcoded and it would be nice to utilize Next's Priority prop to preload the top-most image in our blog which renders 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={image.properties.alt}
/>
)
}
return <p>{paragraph.children}</p>
},
tsx

We are declaring a metaHeight and metaWidth which will look for curly braces in the alt tag and return an array with a string for our height and width based on the numbers we entered between the curly braces and the letter x: {768x432}. These variables will also be used for error handling.

Next, we declare width and height which give us the value for our Image component height and width attributes and utilizing ternaries, we set fallbacks if there are no dimensions declared in a metastring.

And finally, we declare alt and strip out the curly braces from the alt text.

#Taking Advantage of the priority Prop

It'd be nice to have some way to apply the Next/Image prop priority to preload above-the-fold images.

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().match('{priority}')
return (
<Image
src={image.properties.src}
width={width}
height={height}
className="postImg"
alt={alt}
priority={isPriority}
/>
)
}
return <p>{paragraph.children}</p>
},
}
tsx

We simply look for the metastring {priority} and then conditionally render the priority prop. Nice!

#Adding an Image Caption

As a bonus, it's nice to add the ability to easily add an image caption from inside the markdown. To do this, we just add a bit more regex to look for a {caption:} metastring. Anything after the colon will be displayed as your image caption.

Notice we are also wrapping the image component with a div. This will allow us to contain the caption together with the image.

Note

Even if you decide not to utilize the caption functionality, I recommend using the wrapping div in order to target with your CSS to add bottom margin or padding. Without this, the Next/Image component sometimes does wonky things.

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

Our code would look 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().match('{priority}')
const hasCaption = metastring?.toLowerCase().includes('{caption:')
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

I would prefer to have used a regex lookbehind for a cleaner and more modern approach to stripping the caption from our metastring. Unfortunately Safari does not support this, so we do something less than ideal here as a workaround:

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

react-markdown's custom component capability is quite powerful. I have chosen to implement the Gatsby style JSON metastring to extend my markdown but you could take the concepts I've presented and run with them in any direction you see fit.

Enjoy this post? Like and share!