Optimize Images in Your MDX Posts with the Next Image Component

5 min read

Images are a great way to enhance your blog posts by clearly communicating ideas with visualizations and to add visual interest to your site. High quality images are great, but we need to take care to optimize our image size so we don't degrade performance. Lucky for us, Next ships with an Image component that handles a lot of the optimization details for us.

You can read all about the Image component in the official docs, but I'll touch on a few key capabilities here. The Image component will serve up an appropriately sized image based on the device/window size, avoid loading an image until it is visible in the viewport, and avoids Cumulative Layout Shift to help improve your Core Web Vitals.

In order to take advantage of the Image component when creating content in markdown, we need to add a bit of additional processing for our markdown. In this article, we'll be walking through all the steps needed to make this happen.

Hosting & Displaying Images

Hosting static assets in a Next site is as easy as putting them in the public directory, right in the root of your project. I like to further organize assets into subdirectories under public. For the case of blog posts, I'll typically have something like a public/posts directory and then I'll further group images into subdirectories named after the post slug. So for a post with the slug hello-world, I'll create a directory at public/posts/hello-world to store the images for that post.

For purposes of this example, you can use any image you'd like, but if you'd prefer, go ahead and grab this placeholder image and put a copy of it in your project at public/posts/hello-world/placeholder.jpg

placeholder example

With that image (or another) in your project, we can drop that into the content/hello-world.md post with the following markdown. The copy in the square brackets is the alt text for the image, followed by the relative file path in parens. In the case of using the public directory, the initial / starts us off with the public directory.

![placeholder](/posts/hello-world/placeholder.jpg)

Running our site and navigating to the hello-world post should now display an image. If you inspect the image element, the markup should look like this.

<img src="/posts/hello-world/placeholder.jpg" alt="placeholder" />

Adding Image Dimensions

In order for the Image component to do its job, it needs to know the dimensions of the source image. Markdown doesn't give us the ability to add the width and height to an image. We could use the standard HTML img tag in our markdown to allow for this, but we'd still have to get the image dimensions and type them in. I'd much rather let the machine do that work so I can focus on writing. To that end, we'll be installing a rehype plugin to determine image dimensions and to apply those attributes to our resulting markup.

Install rehype-img-size

Let's install the plugin with the following command.

npm i -D rehype-img-size

Configure rehype-img-size

With that installed, let's open up the utils.ts file and add the following import to the top of the file.

import imageSize from 'rehype-img-size'

Now we can find the getPostBySlug function and update the mdxOptions.rehypePlugins array with the configuration for rehype-img-size like so.

export async function getPostBySlug(slug: string) {
  const source = readFileBySlug(slug)
  const { content, data } = matter(source)
  const mdxSource = await serialize(content, {
    mdxOptions: {
      rehypePlugins: [
        [rehypeSlug as any],
        [rehypeAutolinkHeadings as any, { behavior: 'wrap' }],
        [rehypeHighlight as any],
+       [imageSize as any, { dir: 'public' }],
      ],
    },
  })
  return { mdxSource, data }
}

We've add the imageSize plugin (based on the name we used during import) and configured the base directory for our images as public.

With this configuration in place, we can reload our post and we should see the image and if you inspect the element, the markup now contains the width and height attributes.

<img
  src="/posts/hello-world/placeholder.jpg"
  alt="placeholder"
  width="640"
  height="435"
/>

Use the Image Component

Now that our image is included and the markup has the required attributes, we just need to convert that from a standard img element to an instance of the Image component.

To do this, let's open up the pages/blog/[slug].tsx file. Add an import for the Image component at the top of the file.

import Image from 'next/image'

Now that we have that, let's add an img key to the components object with the following implementation.

const components = {
  HotTip,
  strong: ({ children }) => (
    <strong className="text-orange-500 font-bold">{children}</strong>
  ),
  a: SmartLink,
+ img: (props) => (
+   <Image
+     {...props}
+     layout="responsive"
+     loading="lazy"
+     className="rounded-md"
+   />
+ ),
}

We're taking the props from the image and spreading them in. These props will include the src and alt from the default markdown processing. It will also include the width and height added via the rehype-img-size plugin. To that, we're also adding the lazy loading mode and responsive layout mode. You can read more about the available options in the Next docs. I've also added the rounded-md class from tailwind to style images with rounded corners.

With that last bit of config in place, the markup for the image will be a bit more involved.

<img
  alt="placeholder"
  src="/_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=3840&q=75"
  decoding="async"
  data-nimg="responsive"
  class="rounded-md"
  style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%"
  sizes="100vw"
  srcset="
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=640&q=75   640w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=750&q=75   750w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=828&q=75   828w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=1080&q=75 1080w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=1200&q=75 1200w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=1920&q=75 1920w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=2048&q=75 2048w,
    /_next/image?url=%2Fposts%2Fhello-world%2Fplaceholder.jpg&w=3840&q=75 3840w
  "
/>

As you can see, the Image component has done quite a bit to the simple image that we added to the markdown. I, for one, am quite happy to not have to do all that work myself each time I want to display an image 😅!