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 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 🚀