An Astro plugin to open external links in a new tab (the accessible way)


When writing Markdown content, I wanted a simple rule: internal links to behave normally, external links to open in a new tab but also, to be accessible.

The “obvious” solution (and why it kinda sucks)

We all know that opening links in a new tab isn’t just:

target="_blank"

If you care about security and accessibility, you end up with something like this:

<a
  href="https://www.keycloak.org/documentation"
  target="_blank"
  rel="noopener noreferrer"
  aria-label="keycloak documentation"
>
keycloak docs
</a>

This is ok but… Man, it’s painfull to read and write… Imagine if you also want to add aria-describedby, that would make it even worse to read. I really wanted to keep my markdown as simple as possible.

Astro supports MDX, so why not I use it ?

Yes, Astro supports MDX, and you could create a helper component like:

<ExternalLink href="...">Docs</ExternalLink>

But I wanted:

  • Plain Markdown, not MDX
  • No custom syntax
  • Accessibility handled automatically
  • Works everywhere Markdown is used

So instead of changing how I write content, I decided to change how Markdown is processed with Astro.

The right layer: a rehype plugin

This post was inspired by Daniele Salvagni’s (opens in a new tab) article on opening external links in a new tab in Astro. I started from a similar idea, but wanted a solution that works in plain Markdown and puts accessibility front and center.

Astro lets you hook directly into the Markdown -> HTML pipeline using rehype plugins.

That’s the perfect place to do the magic. So I built a small plugin that does exactly that.

What does it do?

For every Markdown link:

  • Leaves internal links alone
  • Detects external links
  • Automatically adds:
    • target="_blank"
    • rel="noopener noreferrer"
    • aria-describedby for screen readers

All while I keep writing normal Markdown

[Astro Docs](https://docs.astro.build)

The plugin

import type { RehypePlugin } from "@astrojs/markdown-remark";
import { visit } from "unist-util-visit";
import type { Element, Parent } from "hast";

const DESCRIBER_ID = "external-link-new-tab";

export const externalLinks: RehypePlugin = ({ domain = "" } = {}) => {
    return (tree) => {
        let hasInsertedNote = false;
        visit(tree, "element", (node: Element, index, parent: Parent | null) => {
            if (
                node.tagName !== "a" ||
                !node.properties?.href ||
                node.properties.target ||
                !parent ||
                typeof index !== "number"
            ) {
                return;
            }

            const href = node.properties.href.toString();
            if (!href.startsWith("http")) return;

            let url: URL;
            try {
                url = new URL(href);
            } catch {
                return;
            }

            const isExternal =
                url.hostname !== domain &&
                !url.hostname.endsWith(`.${domain}`);

            if (!isExternal) return;

            node.properties.target = "_blank";
            node.properties.rel = "noopener noreferrer";
            node.properties["aria-describedby"] = DESCRIBER_ID;

            if (!hasInsertedNote) {
                parent.children.splice(index + 1, 0, {
                    type: "element",
                    tagName: "span",
                    properties: {
                        id: DESCRIBER_ID,
                        className: ["sr-only"],
                    },
                    children: [
                        {
                            type: "text",
                            value: "(opens in a new tab)",
                        },
                    ],
                });

                hasInsertedNote = true;
            }
        });
    };
};

Adding it to Astro config:

import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
import { externalLinks } from "./src/plugins/externalLinks";

export default defineConfig({
  site: "https://projectbrackets.com/",
  integrations: [sitemap()],
  markdown: {
    rehypePlugins: [
      [externalLinks, { domain: "projectbrackets.com" }],
    ],
  },
});

Happy coding!