πŸ“
Blog articleA deep-dive guide from the Domain Extractor Online team. Links back to the tool at the bottom.

How to Build a Chrome Extension for Domain Extraction

Published August 27, 2026 Β· 15 min read Β· By Domain Extractor Team

We get asked all the time whether we have a Chrome extension. The short answer today is not yet β€” the browser tool works great, and we're waiting to make sure the extension solves a real problem beyond convenience. But the whole thing is a small enough project that you can build your own in a couple of hours. This tutorial shows how, using the same extraction algorithm the tool at domainextractoronline.com uses.

What we're building

A minimal Chrome extension that, when you click its toolbar icon on any page, shows a popup listing every unique domain referenced by that page's HTML. Copy-to-clipboard and CSV export included.

Total code: about 200 lines across four files. No build step. Manifest V3, so it will be accepted by the Chrome Web Store today (Chrome removed Manifest V2 support in June 2025).

Prerequisites

  • Chrome (or any Chromium-based browser: Edge, Brave, Arc, Opera)
  • A text editor
  • Basic JavaScript
  • Optional: a Chrome Web Store developer account ($5 one-time) for publishing

Project structure

domain-extractor-ext/
β”œβ”€β”€ manifest.json       ← Extension configuration
β”œβ”€β”€ content.js          ← Runs on each page, extracts domains
β”œβ”€β”€ popup.html          ← The toolbar-icon popup UI
β”œβ”€β”€ popup.js            ← Handles popup interactions
β”œβ”€β”€ icon16.png          ← Toolbar icon (16Γ—16)
β”œβ”€β”€ icon48.png          ← Extensions page icon (48Γ—48)
└── icon128.png         ← Web Store icon (128Γ—128)

Step 1 β€” The manifest

manifest.json tells Chrome what your extension is and what it's allowed to do:

{
  "manifest_version": 3,
  "name": "Domain Extractor",
  "version": "1.0.0",
  "description": "Extract every unique domain referenced by the current page.",
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icon16.png",
      "48": "icon48.png",
      "128": "icon128.png"
    }
  },
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "permissions": ["activeTab", "scripting"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

Key choices:

  • activeTab gives permission to interact with the currently-active tab only when the user clicks the extension icon. This is much less invasive than requesting <all_urls> upfront and users are more likely to install it.
  • scripting lets the popup ask Chrome to execute code in the tab.
  • content_scripts auto-injects content.js into every page so it's ready when the popup opens. If you want to be more conservative, remove this and use chrome.scripting.executeScript from the popup instead.

Step 2 β€” The content script

content.js runs in the page's context and scans everything visible or referenced:

// content.js β€” runs inside each page
(function() {
  const HOSTNAME_RE = /(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}/gi;
  const EMAIL_RE = /[a-zA-Z0-9._-]+@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi;
  function extractDomains() {
    // Sources to scan
    const sources = [];
    // 1. Href attributes
    document.querySelectorAll('a[href], link[href], area[href]').forEach(el => {
      sources.push(el.href);
    });
    // 2. src attributes
    document.querySelectorAll('[src]').forEach(el => sources.push(el.src));
    // 3. Visible text
    sources.push(document.body.innerText);
    // Extract and clean
    const allText = sources.join(' ');
    const found = new Set();
    // Hostname matches
    (allText.match(HOSTNAME_RE) || []).forEach(raw => {
      const clean = raw
        .replace(/^https?:\/\//i, '')
        .replace(/^www\./i, '')
        .replace(/[\/:?#].*$/, '')
        .replace(/[.,;!?]+$/, '')
        .toLowerCase();
      if (clean) found.add(clean);
    });
    // Email domain matches
    (allText.matchAll(EMAIL_RE)).forEach(m => {
      found.add(m[1].toLowerCase());
    });
    return [...found].sort();
  }
  // Listen for messages from popup
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg.type === 'extract') {
      sendResponse({ domains: extractDomains() });
    }
    return true;  // async response
  });
})();

This is intentionally simpler than the full domainextractoronline.com engine β€” no PSL, no TLD validation. Add those if you need higher precision.

Step 3 β€” The popup

popup.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { width: 320px; margin: 0; padding: 12px; font: 13px system-ui; }
    h1 { font-size: 14px; margin: 0 0 8px; }
    #list { max-height: 400px; overflow: auto; border: 1px solid #ddd; padding: 4px; }
    .item { padding: 4px 6px; border-bottom: 1px solid #f1f1f1; font-family: monospace; }
    .btn { margin-top: 8px; padding: 6px 10px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Domain Extractor</h1>
  <div id="count">Scanning…</div>
  <div id="list"></div>
  <button class="btn" id="copyBtn">Copy All</button>
  <button class="btn" id="csvBtn">Download CSV</button>
  <script src="popup.js"></script>
</body>
</html>

popup.js:

let currentDomains = [];
async function loadDomains() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  chrome.tabs.sendMessage(tab.id, { type: 'extract' }, (response) => {
    if (chrome.runtime.lastError) {
      document.getElementById('count').textContent = 'Cannot access this page.';
      return;
    }
    currentDomains = response.domains || [];
    render();
  });
}
function render() {
  document.getElementById('count').textContent = `${currentDomains.length} domains`;
  document.getElementById('list').innerHTML = currentDomains
    .map(d => `<div class="item">${d}</div>`).join('');
}
document.getElementById('copyBtn').addEventListener('click', () => {
  navigator.clipboard.writeText(currentDomains.join('\n'));
});
document.getElementById('csvBtn').addEventListener('click', () => {
  const blob = new Blob([currentDomains.join('\n')], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'domains.csv';
  a.click();
  URL.revokeObjectURL(url);
});
loadDomains();

Step 4 β€” Icons

Any square PNG will work. For a placeholder, generate three sizes: 16Γ—16, 48Γ—48, 128Γ—128. A simple magnifying glass or globe icon is fine to start. If you're publishing, invest in a real icon later β€” it's the first thing users see.

Step 5 β€” Load it in Chrome

  1. Open chrome://extensions
  2. Toggle Developer mode (top right)
  3. Click Load unpacked
  4. Select your domain-extractor-ext folder
  5. The extension icon appears in your toolbar. Click it on any page.

You should see a list of every domain referenced by the current page. Reload the extension after any code change from the same chrome://extensions screen.

Step 6 β€” Publish to the Chrome Web Store

  1. Sign up at Chrome Web Store Developer Dashboard ($5 one-time)
  2. Zip your extension folder (not the parent β€” the files should be at the zip root)
  3. Click New Item and upload the zip
  4. Fill in the store listing: description, screenshots (at least one 1280Γ—800), a promotional tile
  5. Justify each permission β€” activeTab and scripting are both easily justifiable ("The extension reads the current page's DOM to extract domain names when the user clicks the icon.")
  6. Submit for review. First review usually takes 1-3 business days.

Enhancements you might add

  • PSL support: Bundle a stripped Public Suffix List and reduce to registrable domain. See our root-vs-subdomain post.
  • Filter options: Toggle between "all domains" and "external only" (excluding the current site).
  • Right-click menu: Add a context menu item to extract domains from selected text.
  • Batch tabs: Extract from every open tab in one shot.
  • Sync clipboard: Use chrome.storage.sync to remember last extraction across devices.
  • Dark mode: Match the user's Chrome theme via prefers-color-scheme.

Firefox and Edge

The extension above will work almost unchanged in Edge (same Chromium base) and Firefox (with minor manifest tweaks β€” browser_specific_settings). Publish to addons.mozilla.org and Microsoft Partner Center the same way.

Or just use the web tool

If you don't want to build and maintain an extension, the online tool covers 95% of the use cases:

Copy the current page's HTML into it and you get the same result as the extension above, minus the toolbar button convenience.

Further reading