Rendering Next.js <Link/> and <Image/> with react-markdown

I use react-markdown to render articles for this blog. Initially I used remark alone to convert markdown files directly to HTML, but this had some drawbacks; since all links were just plain anchor tags, I lost out on the benefits of client-side navigation provided by Next.js <Link />, and rendering images at specific sizes wasn't possible without additional plugins.

react-markdown converts markdown into React components. As part of that conversion, you can specify custom components to use for specific HTML elements.

import { ReactElement } from "react"
import Link from "next/link"
import Markdown from "react-markdown"

const content = "This markdown has [a link](/)!"

export function PostBody(): ReactElement {
  return (
    <Markdown
      components={{
        a: ({href = '', children}) => {
          return href.startsWith("/") ? (
            // Linking to the site itself
            <Link href={href}>
              {children}
            </Link>
          ) : (
            // Linking elsewhere
            <a href={href} target="_blank" rel="noreferrer">
              {children}
            </a>
          )
        },
      }}
    >
      {content}
    </Markdown>
  )
}

The components prop here specifies that for an anchor tag, depending on the link's href we'll either render an <a> tag that opens in a new tab or a <Link /> from next/link.

  • this link will client side navigate with no page reload, and
  • this link will leave the site

This can also be extended to render images using <Image /> from next/image. Since there isn't a standard format to define image dimensions in Markdown when using the link format, and using the HTML img tag wasn't converted properly for me, I decided to add some search params to the image source and parse them in the components config:

import { ReactElement } from "react"
import Link from "next/link"
import Markdown from "react-markdown"

const content = "![an image](/assets/nextjs-components-from-markdown/example.gif?width=300&height=300";

export function PostBody(): ReactElement {
  return (
    <Markdown
      components={{
        img: ({ src = "", alt = "" }) => {
          // split the image src into the actual file path
          // and params which contain additnoal info
          const [path, params] = src.split("?");
          // extract image dimensions from search params
          const searchParams = new URLSearchParams(params);
          const width = parseInt(searchParams.get("width") ?? "500");
          const height = parseInt(searchParams.get("height") ?? "500");
          // Render an optimised image!
          return <Image src={path} alt={alt} width={width} height={height} />;
        },
      }}
    >
      {content}
    </Markdown>
  )
}

All the benefits of the Next.js <Link /> and <Image />, with the flexibility of Markdown 🚀

an image