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 -
Function that takes in the <a/>
element and returns a ReactElement
.
1 const 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
the renderers
object, where we wire up the tag, the keys in the object, with its corresponding transformation.
1 const renderers : { [ nodeType : string ] : RendererFunction } = {
2 a : ( { href , children } ) : React . ReactElement => renderLink ( { href , children } ) ,
3 } ;
tsx
Wire it all up in the component
1 export 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
4 import { expect , test } from "bun:test" ;
5 import { render , cleanup } from "@testing-library/react" ;
6 import { MarkdownContent } from "./RenderMarkdown" ;
7
8 test ( "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
18 test ( "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