A Markdown to Html transformation layer with `react-markdown`

I'm super pleased with how much control this gives me over my markdown render, which also being easy to use and unit test

In my blog improvement travels, I identified that I wanted external links to open in new tab. "But how can I do that?" I asked myself, since there's no special syntax for that in markdown.

The answer was to use MarkdownContent and the remarkGfm plugin.

From there, I was able to intercept the output of the render and conditionally add target="_blank" when the href starts with https://.

The code in 3 parts -

  1. Function that takes in the <a/> element and returns a ReactElement.
1const renderLink = ({ 2 href, 3 children, 4}: { 5 href: string; 6 children: React.ReactElement; 7}) => { 8 const linkProps: { 9 href: string; 10 ["aria-label"]: string; 11 target?: "_blank"; 12 rel?: "noopener noreferrer"; 13 } = { href, ["aria-label"]: `link to ${href}` }; 14 if (href.startsWith("https://")) { 15 linkProps.target = "_blank"; 16 linkProps.rel = "noopener noreferrer"; 17 } 18 return <a {...linkProps}>{children}</a>; 19};
tsx
  1. the renderers object, where we wire up the tag, the keys in the object, with its corresponding transformation.
1const renderers: { [nodeType: string]: RendererFunction } = { 2 a: ({ href, children }): React.ReactElement => renderLink({ href, children }), 3};
tsx
  1. Wire it all up in the component
1export const MarkdownContent: React.FC<MarkdownContentProps> = ({ 2 content, 3}) => ( 4 <ReactMarkdown 5 components={renderers as any} 6 remarkPlugins={[remarkGfm]} 7 className="py-8" 8 > 9 {content} 10 </ReactMarkdown> 11);
tsx

And it's incredibly testable!

Just look how straight forward this is.

1/// <reference lib="dom" /> 2 3 4import { expect, test } from "bun:test"; 5import { render, cleanup } from "@testing-library/react"; 6import { MarkdownContent } from "./RenderMarkdown"; 7 8test("link with internal href opens locally ", async () => { 9 const testHref = "anyLocalLink"; 10 const testLinkMarkdown = `[any string](${testHref})`; 11 const { getByRole } = render(<MarkdownContent content={testLinkMarkdown} />); 12 const link = await getByRole("link"); 13 expect(link.attributes.getNamedItem("href")?.value).toBe(testHref); 14 expect(link.attributes.getNamedItem(target")?.value).toBeUndefined(); 15 expect(link.attributes.getNamedItem("rel")?.value).toBeUndefined() 16}); 17 18test("link with enteral href opens to new tab ", async () => { 19 cleanup(); 20 const testHref = "https://example.com"; 21 const testLinkMarkdown = `[any string](${testHref})`; 22 const { getByRole } = render(<MarkdownContent content={testLinkMarkdown} />); 23 const link = await getByRole("link"); 24 expect(link.attributes.getNamedItem("href")?.value).toBe(testHref); 25 expect(link.attributes.getNamedItem("target")?.value).toBe("_blank"); 26 expect(link.attributes.getNamedItem("rel")?.value).toBe( 27 "noopener noreferrer" 28 ); 29});
tsx

Relevant Commits