Using HTMR to bring life to links in Gatsby

15th May 2019

One of the amazing things about Gatsby is that your site is a full blown React app. Because of this it's always bugged me that whilst the navigation within app is extremely fast anything I link to from my markdown triggers a full request for the page.

This happens because the links in markdown are just an a tag placed on the page from dangerouslySetInnerHTML. This means no event listeners or anything, just a plain old fashioned link. The solution is to parse the HTML string and swap ot the links for the proper React components.

There are a few HTML to React converters but I found HTMR easy to use for my needs. I'm only looking to swap out a tags for a Link component, not binding events or anything fancy like that.

I already have a Content component that takes a html prop. I created it for just such a case as this where I want to change the way content is rendered across the whole site.

1import React, {ReactHTMLElement} from 'react'
2import convert from 'htmr'
3import {HtmrOptions} from 'htmr/lib/types'
4import {OutboundLink} from 'gatsby-plugin-google-gtag'
5import {Link} from 'gatsby'
7export const Content: React.FunctionComponent<{html: string}> = ({html}) => {
8 const transform: HtmrOptions["transform"] = {
9 a: (node: Partial<ReactHTMLElement<HTMLAnchorElement>["props"]>) => {
10 const {href} = node
12 if(href!.substr(0, 4) === 'http'){
13 return <OutboundLink href={href!}>{node.children}</OutboundLink>
14 }
16 return <Link to={href!}>{node.children}</Link>
17 }
18 }
20 return <div>{convert(html, { transform })}</div>
Language typescript

So what is this doing?

convert comes from HTMR and performs the actual conversion. I've put it in a div tag here to deal with the edge case of html being a string with no tags in it. To bring life to links I used the optional transform option that you can pass to the convert function. From here you can map tags to other tags e.g. b: 'strong' to use strong instead of b for all b tags. Or you can provide a replacement component, button: Button to use a styled button instead. The third usage method is to provide a function.

The function is passed a single argument which is the props that HTMR would use for the orginal tag.

For Example:

1// HTML
2<a href="/something">a link</a>
3// Props
4{href: '/something', children: 'a link'}
Language html

In the code above I do a check of the href prop to see if this is an internal link, or an external link. If its an external link it is replaced with an OutboundLink which makes sure Google Analytics handles the link correctly. Otherwise it must be an internal link which gets replaced with a Link.

The outcome of all this is much faster internal links.

Take this link to about me it is an internal link that is now a proper Link. Give it a try and notice how fast it is. This link to my uses page on the other hand, thats a full blown link and works like internal links did before HTMR.

There is the plugin gatsby-plugin-catch-links that provides a similar outcome but does it in a very heavy handed way. It binds itself to all links in the browser and on a per-link basis decides if its an internal link and how to navigate to its target. My method with HTMR fixes the links during site build and requires no extra even bindings on the client side. Plus it only applies to the markdown and not other links on the site that are already React components.