πŸ“‘Coffee's Blog

Markdown: Opening External Links in New Tabs

CoffeebankπŸ—“οΈOctober 31, 2024

Markdown is a simple, straightforward type of text file that lets you add bold, italics, headings, tables, images, and more.

On its own, it can be perfect for most websites. It's easy to update text and add new pages. A simple website converted from Markdown can load fast, work on all devices, and even be 100% accessible.

Website Links

One major use case for blogs and documentation sites is the need for links. Not only internal links between pages, but also external links to outside websites.

Official practice suggests to only open links in new tabs/windows when necessary. However, from industry experience, the UX for the user seems to be precisely the opposite. Let's look at a few examples:

  • Your blog post is a guide with multiple steps. Each step may contain multiple links to background information, which isn't required but may provide helpful context. These tabs are best opened in the background by default, as the user's main intent is to continue browsing the guide on your site.
  • Opening in a new tab minimizes surprise. When you navigate between pages you control (ie. pages between your website), you may use SSR, loading skeletons, and browser caching to improve the performance and usability of your site. Navigating outside this bubble abruptly changes the environment with a white screen and can surprise the user.
  • Opening in a new tab minimizes harm. Containing the external site in a new tab gives power to the user. Either tab can be closed at the user's discretion, rather than the site owner forcibly redirecting the user away to a completely unrelated site. External websites can also hijack the back button or create redirect loops.

An internal link should represent maintaining the same context for the user. If you are looking up information on the <iframe> element on MDN, you expect the link for the src attribute to maintain the same context.

An external link should represent a new context, separate from their session with your website. When MDN adds a quote by the W3C, the link is a side quest for the user. Their main goal is to learn about accessibility from MDN, and the external link represents a new context.

It can be disorienting for the user when these fundamentally different expectations both open in the same tab/context.

Solutions for external links

One solution for many sites (such as MDN itself) is to use a box-with-arrow icon and target="_blank". This indicates to the user that a link is external and that it will open in a new tab, window, or context.

Noopener Noreferrer

While rel="noopener noreferrer" used to be important for links using target="_blank", noopener is now set by default and is not necessary anymore.

(Optional) noreferrer prevents the external website from seeing that their visitor came from your site.


Markdown Links

Because of Markdown's simplicity, only <a href="https://example.com">Example Link</a> is rendered for links.

This can be useful for basic usage, but on its own, can fall short of expectations for blogs and documentation sites.

Here are some ways to add target="_blank" automatically for websites running a Markdown parser:

Markdown-it

A very popular Markdown parser for websites is markdown-it. It also supports plugins, of which there are many actively supported by the community.

The markdown-it-link-attributes plugin can help us add target="_blank" only to external links.

To install, first run:

npm install markdown-it-link-attributes --save

Then, you can pass an object with an attrs property:

const md = require("markdown-it")();
const mila = require("markdown-it-link-attributes");

md.use(mila, {
  matcher(href) {
    return href.match(/^https?:\/\//);
  },
  attrs: {
    target: "_blank",
    rel: "noopener",
  },
});

var result = md.render("[Example](https://example.com");

result;
// <a href="https://example.com" target="_blank" rel="noopener">Example</a>
// Only for links with http:// or https:// (external links)

Markdown in 11ty (Eleventy)

In Eleventy >= 2.0, you can pass in markdown-it-link-attributes using the amendLibrary Configuration API method:

// .eleventy.js

const mila = require("markdown-it-link-attributes");

...

module.exports = function (eleventyConfig) {
  
  ...

  eleventyConfig.amendLibrary("md", (mdLib) => mdLib.use(mila, {
    matcher(href) {
      return href.match(/^https?:\/\//);
    },
    attrs: {
      target: "_blank",
      rel: "noopener",
    },
  }));
};

MDX in React and Next.js

This blog uses Next.js and MDX, and this tutorial will include a small box-with-arrow icon.

Because this blog also uses Tailwind CSS, we will be using @heroicons/react by Tailwind Labs.

First, set up your MDX (including your mdx-components.tsx file) if you haven't yet.

After your MDX is set up, install:

npm install @heroicons/react

Then, in your main directory, update your mdx-components.tsx:

// mdx-components.tsx

import type { MDXComponents } from 'mdx/types'
import Link from 'next/link'
import { ArrowTopRightOnSquareIcon, LinkIcon } from '@heroicons/react/20/solid'
 
// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.
 
// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // Allows customizing built-in components, e.g. to add styling.
    ...components,
    a: ({ href, children }) => (
      <>
        {
          ( href?.includes("http") || (children as string).toString().includes("http") ) ? (
            <a href={href || (children as string)} target="_blank" rel="noopener" className="inline">
              { children || href }
              <span className="inline pl-1 select-none aria-hidden">
                <ArrowTopRightOnSquareIcon className="inline w-3 h-3" />
              </span>
            </a>
          ) : (
            <Link href={ href || (children as string) || '' }>{ children || href }</Link>
          )
        }
      </>
    )
  }
}