Build a Broken Link Checker Extension: Complete Chrome Extension Development Guide

25 min read

Build a Broken Link Checker Extension: Complete Chrome Extension Development Guide

Build a Broken Link Checker Extension: Complete Chrome Extension Development Guide

Broken links are one of the most frustrating issues web developers, content managers, and SEO specialists face daily. Whether you are maintaining a personal blog or managing a large corporate website, dead links can damage user experience, harm search engine rankings, and make your content appear neglected. In this comprehensive tutorial, we will walk through building a powerful broken link checker extension that runs directly in your Chrome browser, giving you the ability to validate links on any webpage with a single click.

This project will teach you essential Chrome Extension development skills while creating a genuinely useful tool for your web development workflow. By the end of this guide, you will have a fully functional link validator chrome extension that can scan pages, identify broken links, categorize them by error type, and present results in an intuitive interface.


Why Build a Broken Link Checker Extension?

Before diving into the code, let’s explore why creating a broken link checker extension is an excellent project for developers at any skill level. Understanding the value proposition helps you make better design decisions and motivates you through the more complex implementation details.

Every website owner understands the frustration of discovering dead links on their pages. These broken links, also known as dead link finder results, create negative user experiences that drive visitors away. From an SEO perspective, search engines like Google penalize websites with excessive broken links because they interpret them as signs of poor maintenance and outdated content.

Traditional link checking tools require you to paste URLs into external services, which limits their utility for everyday browsing. A browser-based link validator chrome extension solves this problem by bringing the checking functionality directly to where you need it most—your web browsing experience.

What You Will Learn

Building this extension teaches you several fundamental Chrome Extension development concepts that apply to virtually any extension project. You will learn how to interact with web pages using content scripts, communicate between different extension components using message passing, make HTTP requests from background scripts, manage user interface with popup windows, and handle asynchronous operations effectively.


Project Architecture Overview

Every well-structured Chrome Extension follows a specific architectural pattern. Understanding this pattern before writing code prevents common mistakes and makes your extension more maintainable.

Extension Components

Our broken link checker extension consists of four main components that work together to provide a seamless user experience. The manifest file serves as the configuration hub, telling Chrome about your extension’s capabilities and permissions. Content scripts run within web pages, extracting all links from the DOM. The background script handles the heavy lifting of checking each link’s status without blocking the user interface. Finally, the popup provides the interface through which users interact with the extension.

This separation of concerns follows Chrome’s best practices and ensures your extension remains responsive even when checking hundreds of links simultaneously. The content script handles DOM manipulation, the background script manages network requests, and the popup displays results—a clean division of labor that makes debugging easier and performance better.

Manifest V3 Requirements

Modern Chrome Extensions must use Manifest V3, which introduces several important changes from the older Manifest V2. Most significantly, background scripts now run as service workers that can be terminated when inactive, and network requests from content scripts are restricted. Our architecture accounts for these requirements by moving all HTTP requests to the background script and using message passing to coordinate between components.


Step-by-Step Implementation Guide

Now let’s build our broken link checker extension. We will create each file systematically, explaining the purpose of every code section.

Creating the Manifest File

Every Chrome Extension starts with the manifest.json file. This configuration file tells Chrome about your extension’s name, version, permissions, and component files. Create a new folder for your extension and add this manifest.json file.

{
  "manifest_version": 3,
  "name": "Link Validator - Broken Link Checker",
  "version": "1.0.0",
  "description": "Find and fix broken links on any webpage with this powerful link validator chrome extension",
  "permissions": [
    "activeTab",
    "scripting",
    "storage"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "images/icon16.png",
      "48": "images/icon48.png",
      "128": "images/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "images/icon16.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  }
}

This manifest requests the minimum permissions necessary for our link validator chrome extension to function. The activeTab permission allows us to access the current tab when the user invokes the extension. The scripting permission lets us inject content scripts to extract links. The host permissions with <all_urls> are necessary because link checking requires making requests to arbitrary domains.

Building the Content Script

The content script runs within the context of web pages and is responsible for extracting all links from the page. Create a file named content.js with the following code:

// content.js - Extracts all links from the current page

function extractLinks() {
  const links = document.querySelectorAll('a[href]');
  const linkData = [];
  
  for (const link of links) {
    const href = link.href;
    const text = link.textContent.trim();
    const anchor = link.id ? `#${link.id}` : '';
    
    // Skip empty links and javascript: links
    if (href && !href.startsWith('javascript:') && !href.startsWith('mailto:')) {
      linkData.push({
        url: href,
        text: text || '[No text]',
        anchor: anchor
      });
    }
  }
  
  // Remove duplicates based on URL
  const uniqueLinks = [];
  const seenUrls = new Set();
  
  for (const link of linkData) {
    if (!seenUrls.has(link.url)) {
      seenUrls.add(link.url);
      uniqueLinks.push(link);
    }
  }
  
  return uniqueLinks;
}

// Listen for messages from the popup or background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'extractLinks') {
    const links = extractLinks();
    sendResponse({ links: links });
  }
  return true;
});

This content script performs several important functions. First, it finds all anchor elements with href attributes on the page. It extracts both the URL and the link text, which helps users understand which links are broken. It also captures any anchor IDs for more precise identification. Finally, it removes duplicates to avoid checking the same URL multiple times.

Creating the Background Script

The background script serves as the bridge between your content script and the network. It receives links from the content script, checks each one, and returns the results. This separation prevents the network operations from blocking the page. Create background.js with this implementation:

// background.js - Handles link checking logic

// Store for managing scan results
let scanResults = {
  total: 0,
  checked: 0,
  working: 0,
  broken: 0,
  links: []
};

// Check a single URL and return its status
async function checkLink(link) {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000);
    
    const response = await fetch(link.url, {
      method: 'HEAD',
      mode: 'no-cors',
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    // In no-cors mode, we can't see the actual status
    // So we assume success if no error occurred
    return {
      url: link.url,
      text: link.text,
      status: 'working',
      statusCode: 0,
      error: null
    };
  } catch (error) {
    return {
      url: link.url,
      text: link.text,
      status: 'broken',
      statusCode: 0,
      error: error.message
    };
  }
}

// Alternative method using fetch with full error handling
async function checkLinkWithFullDetails(link) {
  const startTime = Date.now();
  
  try {
    const response = await fetch(link.url, {
      method: 'GET',
      cache: 'no-store',
      redirect: 'follow'
    });
    
    const duration = Date.now() - startTime;
    
    if (response.ok) {
      return {
        url: link.url,
        text: link.text,
        status: 'working',
        statusCode: response.status,
        statusText: response.statusText,
        duration: duration,
        error: null
      };
    } else if (response.status >= 300 && response.status < 400) {
      // Handle redirects - follow them or report redirect status
      return {
        url: link.url,
        text: link.text,
        status: 'redirect',
        statusCode: response.status,
        statusText: response.statusText,
        duration: duration,
        error: null
      };
    } else {
      return {
        url: link.url,
        text: link.text,
        status: 'broken',
        statusCode: response.status,
        statusText: response.statusText,
        duration: duration,
        error: `HTTP Error: ${response.status}`
      };
    }
  } catch (error) {
    const duration = Date.now() - startTime;
    
    // Determine if it's a network error or a DNS error
    let errorType = 'network';
    if (error.name === 'AbortError') {
      errorType = 'timeout';
    } else if (error.message && error.message.includes('Failed to fetch')) {
      errorType = 'network';
    }
    
    return {
      url: link.url,
      text: link.text,
      status: 'broken',
      statusCode: 0,
      statusText: '',
      duration: duration,
      error: `${errorType}: ${error.message}`
    };
  }
}

// Message handler for link checking requests
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'checkLinks') {
    const links = request.links;
    
    // Initialize scan results
    scanResults = {
      total: links.length,
      checked: 0,
      working: 0,
      broken: 0,
      redirect: 0,
      links: []
    };
    
    // Process links and send progress updates
    const processLinks = async () => {
      const results = [];
      
      for (let i = 0; i < links.length; i++) {
        const result = await checkLinkWithFullDetails(links[i]);
        results.push(result);
        
        // Update counters
        scanResults.checked = i + 1;
        if (result.status === 'working') {
          scanResults.working++;
        } else if (result.status === 'broken') {
          scanResults.broken++;
        } else if (result.status === 'redirect') {
          scanResults.redirect++;
        }
        
        // Send progress update to popup
        chrome.runtime.sendMessage({
          action: 'progress',
          progress: scanResults
        });
      }
      
      scanResults.links = results;
      
      // Send final results
      return scanResults;
    };
    
    processLinks().then(results => {
      sendResponse({ results: results });
    });
    
    return true;
  }
  
  if (request.action === 'getResults') {
    sendResponse({ results: scanResults });
    return true;
  }
});

This background script implements robust error handling for our dead link finder functionality. It distinguishes between working links, broken links, and redirects, providing detailed error messages that help users understand why specific links failed. The timeout handling prevents the extension from hanging on unresponsive servers.

Designing the Popup Interface

The popup provides the user interface for your extension. Create popup.html with a clean, functional design:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Link Validator</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      width: 400px;
      min-height: 300px;
      background: #ffffff;
      color: #333;
    }
    
    .header {
      background: #4285f4;
      color: white;
      padding: 16px;
      text-align: center;
    }
    
    .header h1 {
      font-size: 18px;
      font-weight: 600;
      margin-bottom: 4px;
    }
    
    .header p {
      font-size: 12px;
      opacity: 0.9;
    }
    
    .actions {
      padding: 16px;
      border-bottom: 1px solid #eee;
    }
    
    .btn {
      width: 100%;
      padding: 12px;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      transition: background 0.2s;
    }
    
    .btn-primary {
      background: #4285f4;
      color: white;
    }
    
    .btn-primary:hover {
      background: #3367d6;
    }
    
    .btn-primary:disabled {
      background: #ccc;
      cursor: not-allowed;
    }
    
    .results {
      padding: 16px;
      max-height: 400px;
      overflow-y: auto;
    }
    
    .stats {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }
    
    .stat {
      flex: 1;
      padding: 12px 8px;
      border-radius: 6px;
      text-align: center;
      background: #f5f5f5;
    }
    
    .stat-value {
      font-size: 20px;
      font-weight: 600;
    }
    
    .stat-label {
      font-size: 11px;
      color: #666;
      text-transform: uppercase;
    }
    
    .stat.working .stat-value {
      color: #34a853;
    }
    
    .stat.broken .stat-value {
      color: #ea4335;
    }
    
    .link-list {
      list-style: none;
    }
    
    .link-item {
      padding: 12px;
      border-radius: 6px;
      margin-bottom: 8px;
      background: #f9f9f9;
      border-left: 3px solid #ccc;
    }
    
    .link-item.working {
      border-left-color: #34a853;
    }
    
    .link-item.broken {
      border-left-color: #ea4335;
    }
    
    .link-text {
      font-size: 13px;
      font-weight: 500;
      margin-bottom: 4px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    
    .link-url {
      font-size: 11px;
      color: #666;
      word-break: break-all;
    }
    
    .link-error {
      font-size: 11px;
      color: #ea4335;
      margin-top: 4px;
    }
    
    .progress-bar {
      height: 4px;
      background: #eee;
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
    }
    
    .progress-fill {
      height: 100%;
      background: #4285f4;
      transition: width 0.3s;
      width: 0%;
    }
    
    .empty-state {
      text-align: center;
      padding: 40px 20px;
      color: #666;
    }
    
    .empty-state-icon {
      font-size: 48px;
      margin-bottom: 16px;
    }
  </style>
</head>
<body>
  <div class="progress-bar">
    <div class="progress-fill" id="progressFill"></div>
  </div>
  
  <div class="header">
    <h1>Link Validator</h1>
    <p>Find broken links on any page</p>
  </div>
  
  <div class="actions">
    <button class="btn btn-primary" id="scanBtn">Scan Current Page</button>
  </div>
  
  <div class="results" id="results">
    <div class="empty-state">
      <div class="empty-state-icon">🔗</div>
      <p>Click "Scan Current Page" to find broken links</p>
    </div>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>

Implementing Popup Logic

The popup JavaScript coordinates between the user interface and the background script. Create popup.js:

// popup.js - Handles user interaction and displays results

document.addEventListener('DOMContentLoaded', () => {
  const scanBtn = document.getElementById('scanBtn');
  const resultsContainer = document.getElementById('results');
  const progressFill = document.getElementById('progressFill');
  
  let isScanning = false;
  
  // Scan button click handler
  scanBtn.addEventListener('click', async () => {
    if (isScanning) return;
    
    isScanning = true;
    scanBtn.textContent = 'Scanning...';
    scanBtn.disabled = true;
    
    // Get the active tab
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    
    if (!tab || !tab.id) {
      showError('No active tab found');
      resetScanButton();
      return;
    }
    
    // Extract links from the page using content script
    try {
      const linkResponse = await chrome.tabs.sendMessage(tab.id, {
        action: 'extractLinks'
      });
      
      if (!linkResponse || !linkResponse.links || linkResponse.links.length === 0) {
        showError('No links found on this page');
        resetScanButton();
        return;
      }
      
      const links = linkResponse.links;
      
      // Send links to background script for checking
      const checkResponse = await chrome.runtime.sendMessage({
        action: 'checkLinks',
        links: links
      });
      
      if (checkResponse && checkResponse.results) {
        displayResults(checkResponse.results);
      }
    } catch (error) {
      showError('Failed to scan page: ' + error.message);
    }
    
    resetScanButton();
  });
  
  // Listen for progress updates from background script
  chrome.runtime.onMessage.addListener((message) => {
    if (message.action === 'progress') {
      const progress = message.progress;
      const percent = (progress.checked / progress.total) * 100;
      progressFill.style.width = percent + '%';
      
      // Update stats while scanning
      updateLiveStats(progress);
    }
  });
  
  function resetScanButton() {
    isScanning = false;
    scanBtn.textContent = 'Scan Current Page';
    scanBtn.disabled = false;
    progressFill.style.width = '0%';
  }
  
  function updateLiveStats(progress) {
    const statsHtml = `
      <div class="stats">
        <div class="stat">
          <div class="stat-value">${progress.total}</div>
          <div class="stat-label">Total</div>
        </div>
        <div class="stat working">
          <div class="stat-value">${progress.working}</div>
          <div class="stat-label">Working</div>
        </div>
        <div class="stat broken">
          <div class="stat-value">${progress.broken}</div>
          <div class="stat-label">Broken</div>
        </div>
      </div>
    `;
    
    const existingStats = resultsContainer.querySelector('.stats');
    if (existingStats) {
      existingStats.outerHTML = statsHtml;
    } else {
      resultsContainer.innerHTML = statsHtml + '<ul class="link-list" id="linkList"></ul>';
    }
  }
  
  function displayResults(results) {
    const workingLinks = results.links.filter(l => l.status === 'working');
    const brokenLinks = results.links.filter(l => l.status === 'broken');
    const redirectLinks = results.links.filter(l => l.status === 'redirect');
    
    let html = `
      <div class="stats">
        <div class="stat">
          <div class="stat-value">${results.total}</div>
          <div class="stat-label">Total</div>
        </div>
        <div class="stat working">
          <div class="stat-value">${results.working}</div>
          <div class="stat-label">Working</div>
        </div>
        <div class="stat broken">
          <div class="stat-value">${results.broken}</div>
          <div class="stat-label">Broken</div>
        </div>
      </div>
    `;
    
    if (brokenLinks.length > 0) {
      html += `<h3 style="margin: 16px 0 8px; font-size: 14px;">Broken Links (${brokenLinks.length})</h3>`;
      html += '<ul class="link-list">';
      
      for (const link of brokenLinks) {
        html += `
          <li class="link-item broken">
            <div class="link-text">${escapeHtml(link.text)}</div>
            <div class="link-url">${escapeHtml(link.url)}</div>
            ${link.error ? `<div class="link-error">${escapeHtml(link.error)}</div>` : ''}
          </li>
        `;
      }
      
      html += '</ul>';
    }
    
    if (workingLinks.length > 0 && brokenLinks.length === 0) {
      html += `
        <div class="empty-state" style="padding: 20px;">
          <div class="empty-state-icon">✅</div>
          <p>All links are working!</p>
        </div>
      `;
    }
    
    resultsContainer.innerHTML = html;
  }
  
  function showError(message) {
    resultsContainer.innerHTML = `
      <div class="empty-state">
        <div class="empty-state-icon">⚠️</div>
        <p>${escapeHtml(message)}</p>
      </div>
    `;
  }
  
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
});

Testing Your Extension

Now that we have created all the necessary files, let’s test our broken link checker extension. First, you need to create placeholder icons since Chrome requires them. Create an images folder and add basic icon files, or simply create empty placeholder files for testing purposes.

Loading the Extension

Open Chrome and navigate to chrome://extensions/. Enable Developer mode using the toggle in the top right corner. Click the “Load unpacked” button and select the folder containing your extension files. Chrome will load your extension and display it in the extensions toolbar.

Navigate to any website with multiple links, such as a blog or news site. Click your extension icon in the toolbar to open the popup. Click “Scan Current Page” and watch as the extension extracts and checks each link. The results will display broken links in red, making them easy to identify and fix.


Advanced Features to Consider

While our basic dead link finder extension works well, several enhancements could make it even more powerful. Consider adding support for checking internal links within the same domain, implementing rate limiting to avoid triggering server blocks, adding export functionality to save results as CSV or JSON, supporting scheduled scans that run automatically, and integrating with sitemap.xml files to check entire websites systematically.


Conclusion

Building a broken link checker extension demonstrates many essential Chrome Extension development concepts while creating a genuinely useful tool. The link validator chrome extension you built follows modern Manifest V3 best practices, implements robust error handling, and provides a clean user interface for identifying dead links.

This broken link checker extension serves as an excellent foundation for more advanced projects. You can extend it with additional features like batch checking, result export, or integration with website management systems. The skills you learned in this tutorial apply directly to building any Chrome Extension, making this project an invaluable addition to your development toolkit.

Remember that maintaining a website free of broken links improves user experience, supports SEO efforts, and demonstrates professional attention to detail. Your dead link finder extension helps achieve these goals efficiently, saving time while ensuring your web presence remains polished and functional.

No previous article
No next article