Convert local image ref in markdown to nextjs-13 <Image /> tag, with remark/rehype/contentlayer tool chain

374 views Asked by At

Markdown files are converting to html OK on my nextjs site. However, images referenced with paths like ![a teapot](./images/teapot.jpg) aren't rendering and show broken images in the browser. When I inspect the source code, I see html like:

<img src="./images/teapot.jpg" alt="a teapot">

Instead of the more complex img tag that nextjs usually generates:

<img alt="teapot" loading="lazy" width="510" height="510" decoding="async" data-nimg="1" class="avatar" srcset="/_next/image?url=%2F_next%2Fstatic%2F..., /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F..." src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F..." style="color: transparent;">

This is the basic shape of my mdx definition in contentlayer.config.js:

  mdx: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [
      rehypeSlug,
      [
        rehypePrettyCode,
        {
          theme: ...,
          onVisitLine(node) {...},
          onVisitHighlightedLine(node) {...},
          onVisitHighlightedWord(node) {...},
        },
      ],
      [
        rehypeAutolinkHeadings,
        {...},
      ],
    ],
  },

What do I need to do to get images to show? I am not sure where the problem lies... is it remark, rehype, contentlayer or nextjs itself? Thanks.

1

There are 1 answers

1
ninjaPixel On
  1. When using a relative path for your image, the path must start with a /, so use ![a teapot](/images/teapot.jpg), rather than ![a teapot](./images/teapot.jpg).

  2. Make sure that your images folder exists in the root public directory of your NextJS project

  3. Remark converts markdown to html. It knows nothing about React or NextJS's Image component. You can tell it what to do with certain elements. I use react-remark within a React component and this is the additional Rehype options required to get it using the Next Image component:

// MyBlog/[slug].jsx
import { useRemarkSync } from "react-remark";

// ...

 export default function Page(props: Props): JSX.Element {
  const { post } = props;
  const content = useRemarkSync(post.markdown || "", {
    rehypeReactOptions: {
      components: {
        img: (props) => {
          const { src, alt } = props;
          return (
            <span className={styles.imgContainer}>
              <Image
                src={src}
                alt={alt}
                fill
                sizes="(min-width: 784px) 784px, 100vw"
              />
            </span>
          );
        },
      },
    },
  });

  // ...

  return <div>{contents}</div>
 }
/* styles.module.css */

.imgContainer{
    display: block;
    position: relative;
    object-fit: cover;
    height: 400px;
}
.imgContainer img{
    object-fit: contain;
}

Alternatively, it's also possible to apply a blanket conversion to all md conversions by using @next/mdx and creating a mdx-components.js at the root of your project (tested on Next v 13) and specifying custom components there. You will need to setup the @next/mdx library, but here is the relevant solution to your particular issue (link to docs):

import Image from 'next/image'
 
// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including inline styles,
// components from other libraries, and more.
 
export function useMDXComponents(components) {
  return {
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...props}
      />
    ),
    ...components,
  }
}