Favicons Next to External Links, Automatically

2 min read

Look at the links in this post. Each one has the icon of the site it points to, like MDN or GitHub. You know where a link goes before you click it.

I wanted that on every article and TIL here, without touching the Markdown. So I built rehype-link-favicons.

The same paragraph and list of links, plain on the left and with a favicon before each external link on the right Before and after. Internal links are left alone.

The Quick Trick, and Why I Didn’t Use It

Google has an undocumented endpoint that returns any site’s favicon:

https://www.google.com/s2/favicons?domain=dev.to&sz=32

Put that in an <img> next to each link and you are done. Except every reader’s browser now makes a request to Google for every link on the page. That is a tracking pixel I don’t want on my blog, and the icons break the day the endpoint changes.

Download Once, at Build Time

The plugin runs while the site builds. For each external link it:

  1. Fetches the icon from Google, falling back to DuckDuckGo.
  2. Caches it on disk, so the next build makes no requests.
  3. Embeds it in the page as a data URI.

Readers only download the page. If a site has no icon or the network is down, the link stays plain and the build carries on.

Using It in Astro

Astro 7 surprised me here. It now renders Markdown with Sätteri, a Rust-based processor, and refuses markdown.rehypePlugins unless you switch back to the old pipeline. So the package also ships a Sätteri version:

// astro.config.mjs
import { satteri } from "@astrojs/markdown-satteri";
import linkFavicons from "rehype-link-favicons/satteri";

export default defineConfig({
  markdown: {
    processor: satteri({ hastPlugins: [linkFavicons()] }),
  },
});

On older Astro, Next.js, or Docusaurus, it is a regular rehype plugin: rehypePlugins: [rehypeLinkFavicons].

Only Where I Want It

Markdown plugins run on every Markdown file. I only want icons in articles and TILs, so I pass a filter that checks the file path:

linkFavicons({
  filter: (_link, file) => /[\\/]src[\\/]content[\\/](blog|til)[\\/]/.test(file.path ?? ""),
});

Three Things I Had to Fix on My Own Site

  • My image styles. Article images here are display: block with big margins. The icons are images too, so without a more specific .prose img.link-favicon rule each one would sit on its own line.
  • The Markdown copies for AI agents. Every page here also has a Markdown copy that the site serves to AI agents. The icons would show up there as images, so I pass properties: { dataMarkdownIgnore: "" } and the step that writes those copies drops them.
  • The RSS feed. The TIL feed embeds full HTML, and feed readers often ignore width and height. A 16-pixel icon showing up at full width is not great, so the feed strips them.

Try It

npm install rehype-link-favicons

The README has setups for Astro, Next.js, Docusaurus, and plain unified, plus recipes for skipping links, Tailwind classes, and caching in CI. Issues and PRs are welcome.

← All posts