Features Automatic link favicons

autoLinkFavicons feature prepends a favicon image (from Google's favicon service) to external links whose text is a plain URL or contains only inline emphasis (<em>/<strong>) — skipping any link already decorated with an marker.

For each qualifying link the link text is cleaned (protocol prefix and trailing slash stripped; domain portion removed when significant path text remains), then wrapped in the original <a> tag preceded by <i><img src="https://www.google.com/s2/favicons?…"></i>. Any HTML already present in the link text is wrapped in a <span> so the favicon image stays a direct child of <a>. Only .html output files are processed.

Compatible with: /css/link-icon ← this link is a live example!


How it works

/anyblades/eleventy-blades/refs/heads/main/features/autoLinkFavicons.js :

export function isExternalUrl(url) {
  return /^https?:\/\//.test(url);
}

export function cleanLinkText(linkText) {
  return linkText
    .trim()
    .replace(/^https?:\/\//, "")
    .replace(/\/$/, "");
}

export function getExtraAttrs(attrs, defaults) {
  return Object.entries(defaults)
    .map(([key, value]) => {
      const regex = new RegExp(`\\b${key}=`, "i");
      return regex.test(attrs) ? "" : ` ${key}="${value}"`;
    })
    .join("");
}

export function buildFaviconLink(attrs, domain, text) {
  const extraAttrs = getExtraAttrs(attrs, {
    title: domain,
    target: "_blank",
    rel: "noopener noreferrer",
  });
  const wrappedText = /<[a-z]/i.test(text) ? `<span>${text}</span>` : text;
  return `<a ${attrs}${extraAttrs}><i><img src="https://www.google.com/s2/favicons?domain=${domain}&sz=64"></i> ${wrappedText}</a>`;
}

export function transformLink(match, attrs, url, linkText) {
  try {
    const domain = new URL(url).hostname;

    if (isExternalUrl(url) && !linkText.includes("↗")) {
      const cleaned = cleanLinkText(linkText);
      const stripped = cleaned.replace(domain, "");
      const label = linkText.trim() === url && stripped.length > 2 ? stripped : cleaned;
      return buildFaviconLink(attrs, domain, label);
    }
  } catch (e) {
    // URL parsing failed — fall through and return original match
  }
  return match;
}

export function replaceLinksInHtml(content, transformer) {
  return content.replace(
    /<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([^<]*(?:<\/?(?:em|strong|code)>[^<]*)*)<\/a>/gi,
    transformer,
  );
}

export default function (eleventyConfig) {
  eleventyConfig.addTransform("autoLinkFavicons", function (content) {
    if (this.page.outputPath && this.page.outputPath.endsWith(".html")) {
      return replaceLinksInHtml(content, transformLink);
    }
    return content;
  });
}