Build Your First AI-Powered Chrome Extension with Manifest V3 and OpenAI

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Chrome extensions are one of the most impactful things you can build as a web developer. They live inside the browser — right where people spend most of their day — and with AI baked in, they become genuinely powerful tools.

In this tutorial, we'll build SummarizeAI, a Chrome extension that can:

  • Summarize any web page with one click
  • Highlight text and get an AI-generated explanation
  • Store summaries locally for later reference
  • Work entirely with Manifest V3 (the current standard)

Why Manifest V3? Google has fully deprecated Manifest V2 as of 2025. All new extensions must use Manifest V3, which introduces service workers instead of background pages, stricter CSP policies, and the chrome.scripting API.

Prerequisites

Before starting, make sure you have:

  • Google Chrome (version 120+)
  • Node.js (v18 or later) — for bundling, not required at runtime
  • An OpenAI API keyget one here
  • Basic knowledge of JavaScript, HTML, and CSS
  • A code editor (VS Code recommended)

Project Architecture

Here's the structure we'll build:

summarize-ai/
├── manifest.json          # Extension manifest (V3)
├── background.js          # Service worker
├── content.js             # Content script (injected into pages)
├── popup/
│   ├── popup.html         # Extension popup UI
│   ├── popup.css          # Popup styles
│   └── popup.js           # Popup logic
├── options/
│   ├── options.html       # Settings page
│   └── options.js         # Settings logic
├── lib/
│   └── ai-client.js       # OpenAI API wrapper
├── icons/
│   ├── icon-16.png
│   ├── icon-48.png
│   └── icon-128.png
└── README.md

Step 1: Create the Manifest

The manifest is the heart of any Chrome extension. Create manifest.json:

{
  "manifest_version": 3,
  "name": "SummarizeAI",
  "version": "1.0.0",
  "description": "AI-powered page summarizer and text explainer",
  "permissions": [
    "activeTab",
    "storage",
    "contextMenus",
    "scripting"
  ],
  "host_permissions": [
    "https://api.openai.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "128": "icons/icon-128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": []
    }
  ],
  "options_page": "options/options.html",
  "icons": {
    "16": "icons/icon-16.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  }
}

Key Manifest V3 Differences

FeatureManifest V2Manifest V3
BackgroundPersistent pageService worker
Network requestswebRequest blockingdeclarativeNetRequest
Remote codeAllowedForbidden
Content SecurityRelaxedStrict by default

In Manifest V3, you cannot load remote scripts. All code must be bundled with the extension. This means no CDN links — everything ships locally.

Step 2: Build the AI Client

Create lib/ai-client.js — this wraps the OpenAI API:

// lib/ai-client.js
 
const DEFAULT_MODEL = "gpt-4o-mini";
 
class AIClient {
  constructor(apiKey, model = DEFAULT_MODEL) {
    this.apiKey = apiKey;
    this.model = model;
    this.baseUrl = "https://api.openai.com/v1/chat/completions";
  }
 
  async complete(systemPrompt, userMessage, options = {}) {
    const { maxTokens = 1000, temperature = 0.3 } = options;
 
    const response = await fetch(this.baseUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify({
        model: this.model,
        messages: [
          { role: "system", content: systemPrompt },
          { role: "user", content: userMessage },
        ],
        max_tokens: maxTokens,
        temperature,
      }),
    });
 
    if (!response.ok) {
      const error = await response.json();
      throw new Error(
        `OpenAI API error: ${error.error?.message || response.statusText}`
      );
    }
 
    const data = await response.json();
    return data.choices[0].message.content;
  }
 
  async summarize(text) {
    return this.complete(
      "You are a concise summarizer. Provide a clear, structured summary of the given text. Use bullet points for key takeaways. Keep it under 200 words.",
      `Summarize the following:\n\n${text}`
    );
  }
 
  async explain(text) {
    return this.complete(
      "You are a helpful explainer. Explain the given text in simple, clear terms. If it's technical, break it down for a general audience.",
      `Explain this:\n\n${text}`,
      { maxTokens: 500 }
    );
  }
}
 
// Export for use across extension contexts
if (typeof globalThis !== "undefined") {
  globalThis.AIClient = AIClient;
}

We use gpt-4o-mini by default because it's fast, cheap, and good enough for summarization. Users can switch to gpt-4o in the settings for higher quality.

Step 3: Set Up the Service Worker (Background Script)

The service worker handles context menus, message passing, and orchestration:

// background.js
 
importScripts("lib/ai-client.js");
 
// Create context menu on install
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "explain-selection",
    title: "Explain with AI",
    contexts: ["selection"],
  });
 
  chrome.contextMenus.create({
    id: "summarize-page",
    title: "Summarize this page",
    contexts: ["page"],
  });
 
  // Set default settings
  chrome.storage.sync.get(["apiKey", "model"], (result) => {
    if (!result.model) {
      chrome.storage.sync.set({ model: "gpt-4o-mini" });
    }
  });
});
 
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  const { apiKey, model } = await chrome.storage.sync.get([
    "apiKey",
    "model",
  ]);
 
  if (!apiKey) {
    chrome.notifications.create({
      type: "basic",
      iconUrl: "icons/icon-48.png",
      title: "SummarizeAI",
      message: "Please set your OpenAI API key in the extension settings.",
    });
    return;
  }
 
  const client = new AIClient(apiKey, model);
 
  if (info.menuItemId === "explain-selection" && info.selectionText) {
    try {
      const explanation = await client.explain(info.selectionText);
      // Send result to content script for display
      chrome.tabs.sendMessage(tab.id, {
        type: "SHOW_RESULT",
        title: "AI Explanation",
        content: explanation,
      });
    } catch (error) {
      chrome.tabs.sendMessage(tab.id, {
        type: "SHOW_ERROR",
        message: error.message,
      });
    }
  }
 
  if (info.menuItemId === "summarize-page") {
    // Inject script to get page content, then summarize
    chrome.scripting.executeScript(
      {
        target: { tabId: tab.id },
        func: () => document.body.innerText,
      },
      async (results) => {
        if (results && results[0]) {
          const pageText = results[0].result.substring(0, 10000);
          try {
            const summary = await client.summarize(pageText);
            chrome.tabs.sendMessage(tab.id, {
              type: "SHOW_RESULT",
              title: "Page Summary",
              content: summary,
            });
          } catch (error) {
            chrome.tabs.sendMessage(tab.id, {
              type: "SHOW_ERROR",
              message: error.message,
            });
          }
        }
      }
    );
  }
});
 
// Handle messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "SUMMARIZE_TAB") {
    handleSummarizeTab(message.tabId).then(sendResponse);
    return true; // Keeps the message channel open for async
  }
 
  if (message.type === "GET_HISTORY") {
    chrome.storage.local.get(["history"], (result) => {
      sendResponse(result.history || []);
    });
    return true;
  }
});
 
async function handleSummarizeTab(tabId) {
  const { apiKey, model } = await chrome.storage.sync.get([
    "apiKey",
    "model",
  ]);
 
  if (!apiKey) return { error: "API key not set" };
 
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => ({
      text: document.body.innerText,
      title: document.title,
      url: window.location.href,
    }),
  });
 
  if (!results || !results[0]) return { error: "Could not read page" };
 
  const { text, title, url } = results[0].result;
  const client = new AIClient(apiKey, model);
 
  try {
    const summary = await client.summarize(text.substring(0, 10000));
 
    // Save to history
    const { history = [] } = await chrome.storage.local.get(["history"]);
    history.unshift({
      title,
      url,
      summary,
      timestamp: Date.now(),
    });
 
    // Keep last 50 entries
    await chrome.storage.local.set({
      history: history.slice(0, 50),
    });
 
    return { summary, title };
  } catch (error) {
    return { error: error.message };
  }
}

Service workers in Manifest V3 are not persistent. They can be terminated after 30 seconds of inactivity. Don't store state in variables — always use chrome.storage.

Step 4: Build the Content Script

The content script injects a floating result panel into web pages:

// content.js
 
(function () {
  let resultPanel = null;
 
  function createPanel() {
    if (resultPanel) return resultPanel;
 
    resultPanel = document.createElement("div");
    resultPanel.id = "summarize-ai-panel";
    resultPanel.innerHTML = `
      <div class="sai-header">
        <span class="sai-title">SummarizeAI</span>
        <button class="sai-close">&times;</button>
      </div>
      <div class="sai-body">
        <div class="sai-loading" style="display:none">
          <div class="sai-spinner"></div>
          <span>Thinking...</span>
        </div>
        <div class="sai-content"></div>
      </div>
    `;
 
    // Apply styles
    const style = document.createElement("style");
    style.textContent = `
      #summarize-ai-panel {
        position: fixed;
        bottom: 20px;
        right: 20px;
        width: 380px;
        max-height: 500px;
        background: #1a1a2e;
        color: #eee;
        border-radius: 12px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.3);
        z-index: 999999;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        font-size: 14px;
        overflow: hidden;
        animation: sai-slide-in 0.3s ease-out;
      }
      @keyframes sai-slide-in {
        from { transform: translateY(20px); opacity: 0; }
        to { transform: translateY(0); opacity: 1; }
      }
      .sai-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 12px 16px;
        background: #16213e;
        border-bottom: 1px solid #0f3460;
      }
      .sai-title {
        font-weight: 600;
        color: #e94560;
      }
      .sai-close {
        background: none;
        border: none;
        color: #888;
        font-size: 20px;
        cursor: pointer;
        padding: 0 4px;
      }
      .sai-close:hover { color: #fff; }
      .sai-body {
        padding: 16px;
        max-height: 400px;
        overflow-y: auto;
        line-height: 1.6;
      }
      .sai-content ul { padding-left: 20px; }
      .sai-content li { margin-bottom: 6px; }
      .sai-spinner {
        width: 20px;
        height: 20px;
        border: 2px solid #333;
        border-top: 2px solid #e94560;
        border-radius: 50%;
        animation: sai-spin 0.8s linear infinite;
        display: inline-block;
        margin-right: 8px;
        vertical-align: middle;
      }
      @keyframes sai-spin {
        to { transform: rotate(360deg); }
      }
      .sai-loading {
        display: flex;
        align-items: center;
        color: #888;
      }
      .sai-error {
        color: #e94560;
        padding: 8px;
        background: rgba(233,69,96,0.1);
        border-radius: 6px;
      }
    `;
 
    document.head.appendChild(style);
    document.body.appendChild(resultPanel);
 
    // Close button
    resultPanel.querySelector(".sai-close").addEventListener("click", () => {
      resultPanel.style.display = "none";
    });
 
    return resultPanel;
  }
 
  function showLoading() {
    const panel = createPanel();
    panel.style.display = "block";
    panel.querySelector(".sai-loading").style.display = "flex";
    panel.querySelector(".sai-content").innerHTML = "";
  }
 
  function showResult(title, content) {
    const panel = createPanel();
    panel.style.display = "block";
    panel.querySelector(".sai-loading").style.display = "none";
    panel.querySelector(".sai-title").textContent = title;
 
    // Convert markdown-like formatting
    const formatted = content
      .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
      .replace(/^- (.*)/gm, "<li>$1</li>")
      .replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
      .replace(/\n/g, "<br>");
 
    panel.querySelector(".sai-content").innerHTML = formatted;
  }
 
  function showError(message) {
    const panel = createPanel();
    panel.style.display = "block";
    panel.querySelector(".sai-loading").style.display = "none";
    panel.querySelector(".sai-content").innerHTML = `
      <div class="sai-error">⚠️ ${message}</div>
    `;
  }
 
  // Listen for messages from background
  chrome.runtime.onMessage.addListener((message) => {
    if (message.type === "SHOW_RESULT") {
      showResult(message.title, message.content);
    }
    if (message.type === "SHOW_ERROR") {
      showError(message.message);
    }
    if (message.type === "SHOW_LOADING") {
      showLoading();
    }
  });
})();

Step 5: Create the Popup UI

The popup is what users see when they click the extension icon.

<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div class="container">
      <header>
        <h1>🤖 SummarizeAI</h1>
      </header>
 
      <div id="no-key-warning" class="warning" style="display: none">
        <p>⚠️ API key not set.</p>
        <a href="#" id="open-settings">Open Settings</a>
      </div>
 
      <div id="main-content">
        <button id="summarize-btn" class="primary-btn">
          ✨ Summarize This Page
        </button>
 
        <div id="result" style="display: none">
          <h3 id="result-title"></h3>
          <div id="result-content"></div>
        </div>
 
        <div id="loading" style="display: none">
          <div class="spinner"></div>
          <span>Analyzing page...</span>
        </div>
 
        <hr />
 
        <h3>📚 Recent Summaries</h3>
        <div id="history-list"></div>
      </div>
    </div>
 
    <script src="popup.js"></script>
  </body>
</html>
/* popup/popup.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
body {
  width: 400px;
  max-height: 550px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #0f0f23;
  color: #eee;
}
 
.container {
  padding: 16px;
}
 
header {
  text-align: center;
  margin-bottom: 16px;
}
 
header h1 {
  font-size: 18px;
  color: #e94560;
}
 
.primary-btn {
  width: 100%;
  padding: 12px;
  background: linear-gradient(135deg, #e94560, #0f3460);
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.1s;
}
 
.primary-btn:hover {
  transform: scale(1.02);
}
 
.primary-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
 
.warning {
  background: rgba(233, 69, 96, 0.1);
  border: 1px solid #e94560;
  padding: 12px;
  border-radius: 8px;
  margin-bottom: 12px;
  text-align: center;
}
 
.warning a {
  color: #e94560;
}
 
#result {
  margin-top: 16px;
  padding: 12px;
  background: #1a1a2e;
  border-radius: 8px;
  max-height: 250px;
  overflow-y: auto;
  line-height: 1.6;
}
 
#result h3 {
  font-size: 14px;
  color: #e94560;
  margin-bottom: 8px;
}
 
#loading {
  text-align: center;
  padding: 20px;
  color: #888;
}
 
.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #333;
  border-top: 3px solid #e94560;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  margin: 0 auto 8px;
}
 
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
 
hr {
  border: none;
  border-top: 1px solid #1a1a2e;
  margin: 16px 0;
}
 
.history-item {
  padding: 10px;
  background: #1a1a2e;
  border-radius: 6px;
  margin-bottom: 8px;
  cursor: pointer;
  transition: background 0.2s;
}
 
.history-item:hover {
  background: #16213e;
}
 
.history-item .title {
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
 
.history-item .date {
  font-size: 11px;
  color: #666;
}
// popup/popup.js
 
document.addEventListener("DOMContentLoaded", async () => {
  const summarizeBtn = document.getElementById("summarize-btn");
  const resultDiv = document.getElementById("result");
  const resultTitle = document.getElementById("result-title");
  const resultContent = document.getElementById("result-content");
  const loadingDiv = document.getElementById("loading");
  const historyList = document.getElementById("history-list");
  const noKeyWarning = document.getElementById("no-key-warning");
 
  // Check for API key
  const { apiKey } = await chrome.storage.sync.get(["apiKey"]);
  if (!apiKey) {
    noKeyWarning.style.display = "block";
    summarizeBtn.disabled = true;
  }
 
  // Open settings link
  document.getElementById("open-settings").addEventListener("click", (e) => {
    e.preventDefault();
    chrome.runtime.openOptionsPage();
  });
 
  // Summarize button
  summarizeBtn.addEventListener("click", async () => {
    const [tab] = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
 
    summarizeBtn.disabled = true;
    loadingDiv.style.display = "block";
    resultDiv.style.display = "none";
 
    chrome.runtime.sendMessage(
      { type: "SUMMARIZE_TAB", tabId: tab.id },
      (response) => {
        loadingDiv.style.display = "none";
        summarizeBtn.disabled = false;
 
        if (response.error) {
          resultTitle.textContent = "Error";
          resultContent.textContent = response.error;
        } else {
          resultTitle.textContent = response.title;
          resultContent.innerHTML = formatContent(response.summary);
        }
 
        resultDiv.style.display = "block";
        loadHistory(); // Refresh history
      }
    );
  });
 
  // Load history
  async function loadHistory() {
    chrome.runtime.sendMessage({ type: "GET_HISTORY" }, (history) => {
      historyList.innerHTML = history
        .slice(0, 10)
        .map(
          (item) => `
        <div class="history-item" data-url="${item.url}">
          <div class="title">${item.title}</div>
          <div class="date">${new Date(item.timestamp).toLocaleDateString()}</div>
        </div>
      `
        )
        .join("");
 
      // Click to open URL
      historyList.querySelectorAll(".history-item").forEach((el) => {
        el.addEventListener("click", () => {
          chrome.tabs.create({ url: el.dataset.url });
        });
      });
    });
  }
 
  loadHistory();
});
 
function formatContent(text) {
  return text
    .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
    .replace(/^- (.*)/gm, "<li>$1</li>")
    .replace(/\n/g, "<br>");
}

Step 6: Build the Options Page

<!-- options/options.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>SummarizeAI Settings</title>
    <style>
      body {
        font-family: -apple-system, sans-serif;
        max-width: 500px;
        margin: 40px auto;
        padding: 20px;
        background: #0f0f23;
        color: #eee;
      }
      h1 { color: #e94560; }
      label { display: block; margin-top: 16px; font-weight: 600; }
      input, select {
        width: 100%;
        padding: 10px;
        margin-top: 6px;
        background: #1a1a2e;
        border: 1px solid #333;
        border-radius: 6px;
        color: #eee;
        font-size: 14px;
      }
      button {
        margin-top: 20px;
        padding: 10px 24px;
        background: #e94560;
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
        font-size: 14px;
      }
      .saved {
        color: #4ecca3;
        margin-left: 12px;
        display: none;
      }
    </style>
  </head>
  <body>
    <h1>⚙️ SummarizeAI Settings</h1>
 
    <label for="apiKey">OpenAI API Key</label>
    <input type="password" id="apiKey" placeholder="sk-..." />
 
    <label for="model">Model</label>
    <select id="model">
      <option value="gpt-4o-mini">GPT-4o Mini (Fast & Cheap)</option>
      <option value="gpt-4o">GPT-4o (Higher Quality)</option>
      <option value="gpt-4.1-nano">GPT-4.1 Nano (Ultra Fast)</option>
    </select>
 
    <button id="save">Save Settings</button>
    <span class="saved" id="saved-msg">✓ Saved!</span>
 
    <script src="options.js"></script>
  </body>
</html>
// options/options.js
 
document.addEventListener("DOMContentLoaded", async () => {
  const apiKeyInput = document.getElementById("apiKey");
  const modelSelect = document.getElementById("model");
  const saveBtn = document.getElementById("save");
  const savedMsg = document.getElementById("saved-msg");
 
  // Load existing settings
  const { apiKey, model } = await chrome.storage.sync.get([
    "apiKey",
    "model",
  ]);
  if (apiKey) apiKeyInput.value = apiKey;
  if (model) modelSelect.value = model;
 
  // Save
  saveBtn.addEventListener("click", () => {
    chrome.storage.sync.set(
      {
        apiKey: apiKeyInput.value.trim(),
        model: modelSelect.value,
      },
      () => {
        savedMsg.style.display = "inline";
        setTimeout(() => (savedMsg.style.display = "none"), 2000);
      }
    );
  });
});

Step 7: Load and Test the Extension

Now let's load the extension in Chrome:

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable "Developer mode" using the toggle in the top-right corner
  3. Click "Load unpacked" and select your summarize-ai/ folder
  4. The extension icon should appear in your toolbar

Testing Workflow

  1. Set your API key: Click the extension icon → notice the warning → click "Open Settings" → enter your key → save
  2. Summarize a page: Navigate to any article → click the extension → click "Summarize This Page"
  3. Explain text: Select any text on a page → right-click → "Explain with AI"
  4. Check history: Open the popup again — your recent summaries appear at the bottom

For debugging, open chrome://extensions/, find SummarizeAI, and click "Service Worker" to inspect the background script. Right-click the popup and choose "Inspect" to debug the popup.

Step 8: Add Keyboard Shortcuts

Make the extension faster with keyboard shortcuts. Add to manifest.json:

{
  "commands": {
    "summarize-page": {
      "suggested_key": {
        "default": "Alt+S",
        "mac": "Alt+S"
      },
      "description": "Summarize the current page"
    },
    "_execute_action": {
      "suggested_key": {
        "default": "Alt+A",
        "mac": "Alt+A"
      },
      "description": "Open SummarizeAI popup"
    }
  }
}

Handle the command in background.js:

chrome.commands.onCommand.addListener(async (command) => {
  if (command === "summarize-page") {
    const [tab] = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
    const result = await handleSummarizeTab(tab.id);
 
    if (result.summary) {
      chrome.tabs.sendMessage(tab.id, {
        type: "SHOW_RESULT",
        title: "Page Summary",
        content: result.summary,
      });
    }
  }
});

Step 9: Handle Edge Cases

Real-world extensions need to handle failures gracefully:

// Add to background.js
 
// Rate limiting
const rateLimiter = {
  lastCall: 0,
  minInterval: 2000, // 2 seconds between calls
 
  canProceed() {
    const now = Date.now();
    if (now - this.lastCall < this.minInterval) {
      return false;
    }
    this.lastCall = now;
    return true;
  },
};
 
// Wrap API calls
async function safeApiCall(fn) {
  if (!rateLimiter.canProceed()) {
    return { error: "Please wait a moment before trying again." };
  }
 
  try {
    return await fn();
  } catch (error) {
    if (error.message.includes("429")) {
      return { error: "Rate limit reached. Please wait a minute." };
    }
    if (error.message.includes("401")) {
      return { error: "Invalid API key. Check your settings." };
    }
    return { error: `Unexpected error: ${error.message}` };
  }
}

Step 10: Prepare for the Chrome Web Store

When you're ready to publish:

1. Create Icons

You'll need icons at 16×16, 48×48, and 128×128 pixels in PNG format. Use a tool like Figma or even an AI image generator.

2. Build a ZIP

# Remove any development files
zip -r summarize-ai.zip summarize-ai/ \
  -x "*.DS_Store" \
  -x "*node_modules*" \
  -x "*.git*"

3. Submit to Chrome Web Store

  1. Go to the Chrome Web Store Developer Dashboard
  2. Pay the one-time $5 registration fee
  3. Click "New Item" and upload your ZIP
  4. Fill out the listing: description, screenshots, category
  5. Submit for review (typically 1-3 business days)

Privacy Policy Required: If your extension sends data to external APIs (like OpenAI), you MUST provide a privacy policy URL. This is enforced by the Chrome Web Store.

Going Further

Here are ideas to extend SummarizeAI:

  • Multiple Languages: Detect page language and summarize in the user's preferred language
  • Custom Prompts: Let users define their own system prompts for specialized summarization
  • PDF Support: Use pdf.js to extract text from PDF files
  • Export: Save summaries as Markdown or send to Notion
  • Streaming: Use OpenAI's streaming API for real-time response display
  • Local AI: Integrate with Ollama for completely offline summarization

Summary

In this tutorial, you built a fully functional AI-powered Chrome extension using Manifest V3. Here's what we covered:

  • Manifest V3 structure and key differences from V2
  • Service workers for background processing
  • Content scripts for injecting UI into web pages
  • OpenAI API integration for summarization and explanation
  • Popup UI with history tracking
  • Context menus and keyboard shortcuts
  • Error handling and rate limiting
  • Chrome Web Store publishing process

The complete source code is available on GitHub. Chrome extensions are a fantastic way to deliver AI directly where users need it — inside their browser. Now go build something amazing. 🚀


Want to read more tutorials? Check out our latest tutorial on Mastering Next.js: Unleashing the Power of Third-Party Integrations.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles