Integrate the Next Link Component with Your MDX Content

5 min read

Next provides the Link component to allow client side navigation. This is great when we're working directly in some JSX for a component or a page, but it's not quite as straightforward when we're trying to create local links in our markdown. We certainly could just expose the Link component to the MDXRemote component and use it directly, but I prefer to use markdown style links.

In this installment of the Build a Blog with Next, MDX, and Tailwind Blog series, we'll create a SmartLink component that uses a standard anchor tag with a target attribute set to _blank for external links and the Link component for local links.

Some Example Content

Let's start off by modifying one of our markdown files. We'll add 3 separate links in the first paragraph. One will link to an external website, another will link to a heading within the document, and the third will link to the route for one of our other markdown based routes.

I'll continue to add updates to the content/hello-world.md file. If you've been following along, this page should have some h2 and h3 elements that allow links to those anchors. My updates will make the first paragraph look like this.

Maybe there's a **happy little bush** that lives [right there](https://vanslaars.io). There's [nothing wrong](/blog/stuff-and-things) with having a tree as a friend. [Mountains are so simple](#section-two), they're hard.

Let's start by adding a new React component file in our components directory called SmartLink.tsx with the following starting content.

import Link from 'next/link'
import {
  AnchorHTMLAttributes,
  DetailedHTMLProps,
  FunctionComponent,
} from 'react'

export const SmartLink: FunctionComponent<
  DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
> = ({ children, href, ...props }) => {
  return null
}

We're typing this specifically with the HTMLAnchorElement type to ensure that anchor properties, like href are accepted by TypeScript. We're extracting children and href so we can separate them from the other props, then we're gathering the remaining props with the rest operator so we can pass them through to the underlying element.

Now we'll update the render with a couple ternaries. Let's lead with the code, then talk through it.

export const SmartLink: FunctionComponent<
  DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
> = ({ children, href, ...props }) => {
  return href.startsWith('http') ? (
    <a href={href} {...props} target="_blank" rel="noreferrer">
      {children}
    </a>
  ) : href.startsWith('#') ? (
    <a href={href} {...props}>
      {children}
    </a>
  ) : (
    <Link href={href}>
      <a {...props}>{children}</a>
    </Link>
  )
}

First we check for the href to start with http. If there is an http or https at the start of the address, we'll return a standard anchor with a target="_blank" attribute and a rel="noreferrer".

If we're not dealing with an external link, we then check for an href that starts with a hash. In that case, we'll return a standard anchor since this is a link to an anchor on the same page.

Finally, if it's not external or a link to an anchor on the same page, it should be a link to another page within our app. In this case, we use the Link component to allow a client side transition.

With that component in place, we need to put it to work for our blog posts. We'll import the SmartLink component into pages/blog/[slug].tsx. And we'll map the a element to the SmartLink in the components object.

import type { GetStaticPaths, GetStaticProps } from 'next'
import Head from 'next/head'
import { getAllPostSlugs, getPostBySlug } from '../../utils'
import { MDXRemoteSerializeResult } from 'next-mdx-remote/dist/types'
import { MDXRemote } from 'next-mdx-remote'
import { HotTip } from '../../components/HotTip'
+import { SmartLink } from '../../components/SmartLink'

const components = {
  HotTip,
  strong: ({ children }) => (
    <strong className="text-orange-500 font-bold">{children}</strong>
  ),
+ a: SmartLink,
}

With that in place, loading up the /blog/hello-world route should render out our 3 links. The external link should launch a new tab, the internal link should jump to the heading, and the local link should navigate with a client transition rather than a full page load.

For external links, it would improve the user experience to indicate that a link will open in a new window. Since we're rendering external links differently from the others inside SmartLink, we'll just go back there and add some extra styling.

export const SmartLink: FunctionComponent<
  DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
> = ({ children, href, ...props }) => {
  return href.startsWith('http') ? (
-   <a href={href} {...props} target="_blank" rel="noreferrer">
+   <a
+     href={href}
+     {...props}
+     target="_blank"
+     rel="noreferrer"
+     className="after:content-['\2197'] after:text-orange-300"
+   >
      {children}
    </a>
  ) : href.startsWith('#') ? (
    <a href={href} {...props}>
      {children}
    </a>
  ) : (
    <Link href={href}>
      <a {...props}>{children}</a>
    </Link>
  )
}

We're using the after variant here to add an arrow that points up and to the right with unicode.

Conclusion

Now we have all the benefits of Next's routing when we need it, support standard browser behavior for anchors, and launch a new tab and provide context clues about link behaviors for external links, all without having to deviate from markdown when creating content.

Hot Tip