Category:

For a WordPress site I needed an external link solution that opens external links in a new tab and adds an icon next to links. WordPress has some plugins that do this but there’s something about each one I don’t like. Or, some have PHP warnings/errors when installing. So, here’s a pure JavaScript solution that I feel is pretty robust yet lightweight.

Since everyone’s setup is different, you can list patterns (that would be in a URL) for the script to ignore. For example, you will see within the code that it currently ignores inline JavaScript Void, and some image extensions:

const ignoredLinks = ['javascript:void(0)', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];

It is also optimized for SEO thanks to the nofollow attribute, and for screen readers, it will tell the user that this link opens in a new window.

(function() {
  'use strict';

  // Define the SVG icon
  const externalLinkIcon = `
    <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link">
      <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
      <polyline points="15 3 21 3 21 9"></polyline>
      <line x1="10" y1="14" x2="21" y2="3"></line>
    </svg>
  `;

  // Get the current hostname
  const currentHostname = window.location.hostname;

  // List of patterns or URLs to ignore (add your patterns here, separated by commas)
  const ignoredLinks = ['javascript:void(0)', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];

  // Function to check if a URL should be ignored
  function isIgnoredLink(url) {
    for (const pattern of ignoredLinks) {
      if (url.includes(pattern)) {
        return true;
      }
    }
    return false;
  }

  // Function to check if a URL is external
  function isExternalLink(url) {
    const parser = document.createElement('a');
    parser.href = url;

    // Check if the link is a hash link (same page)
    if (parser.hash && parser.hostname === currentHostname) {
      return false;
    }

    // Check if the link is external and not ignored
    return parser.hostname !== currentHostname && !isIgnoredLink(url);
  }

  // Function to handle external links
  function handleExternalLink(event) {
    const link = event.target.closest('.post a:not([href^="#"]),.page a:not([href^="#"]),.single a:not([href^="#"]),.archive a:not([href^="#"])');

    if (link && isExternalLink(link.href)) {
      event.preventDefault();
      link.setAttribute('rel', 'noopener noreferrer');
      link.setAttribute('target', '_blank');
      window.open(link.href, '_blank');
    }
  }

  // Attach the event listener
  const contentAreas = document.querySelectorAll('.post, .page, .single, .archive');
  contentAreas.forEach(area => area.addEventListener('click', handleExternalLink, false));

  // Use a MutationObserver to handle dynamically added links
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          const externalLinks = node.querySelectorAll('.post a:not([href^="#"]):not([rel="noopener noreferrer"]),.page a:not([href^="#"]):not([rel="noopener noreferrer"]),.single a:not([href^="#"]):not([rel="noopener noreferrer"]),.archive a:not([href^="#"]):not([rel="noopener noreferrer"])');

          if (externalLinks.length > 0) {
            externalLinks.forEach((link) => {
              if (isExternalLink(link.href)) {
                link.setAttribute('rel', 'noopener noreferrer');
                link.setAttribute('target', '_blank');
                const externalIcon = document.createElement('span');
                externalIcon.innerHTML = externalLinkIcon;
                externalIcon.classList.add('external-link-icon');
                externalIcon.setAttribute('aria-label', 'Opens in new tab');
                externalIcon.setAttribute('title', 'Opens in new tab');
                link.insertAdjacentElement('afterend', externalIcon);
              }
            });
          }
        }
      });
    });
  });

  contentAreas.forEach(area => observer.observe(area, { childList: true, subtree: true }));
})();


Leave a Reply

Your email address will not be published. Required fields are marked *

Billy Wilcosky