Ver en Español
How to Use Content Scripts in Chrome Extensions
Jun 24, 2026
Updated: Jun 25, 2026

How to Use Content Scripts in Chrome Extensions

In this post we'll see how to work with content scripts in Chrome extensions. Content scripts are files that run in the context of a web page and let you modify its content. We'll explore how to inject these scripts, communicate with the background script, and customize their execution across different websites.

What Are Content Scripts?

Content scripts are JavaScript files that run in the context of the web page, letting you manipulate the DOM of the page they're injected into. These scripts live in an isolated world, which means they can interact with the page without interfering with other scripts the page might be running.

Basic Content Script Setup

The first thing to do is inject our content script. For that you need to configure your manifest.json to include the content scripts. Here's an example:

{
  "manifest_version": 3,
  "name": "Mi Extensión",
  "version": "0.0.1",
  "content_scripts": [
    {
      "matches": ["*://*.google.com/*"],
      "js": ["content-script.js"]
    }
  ]
}

This file specifies that content-script.js will be injected into all Google pages.

Now create a file called content-script.js with the following content:

alert('Hola, soy un Content Script');

To verify the injection, load your extension in Chrome and navigate to google.com. You should see the alert message appear, indicating that the content script was injected correctly.

Communication Between Content Scripts and Background Scripts

For more advanced tasks, content scripts need to communicate with the background script. Let's see how to send and receive messages between them.

First, we send a message from the content script. Modify your content-script.js to send a message to the background script:

chrome.runtime.sendMessage({ greeting: "hello" }, function (response) {
  console.log(response.farewell);
});

Then, we receive the message in the background script. Modify your background.js to listen for the message and respond:

chrome.runtime.onMessage.addListener(
  function (request, sender, sendResponse) {
    if (request.greeting === "hello")
      sendResponse({ farewell: "goodbye" });
  }
);

To test the communication, reload the extension and check the console on the Google page to see the response message.

Customizing Content Scripts

You can customize when and where content scripts run using match and exclude patterns.

With match patterns you can specify which URLs the content scripts should be injected into using the matches attribute. Here's an example for injecting into multiple sites:

"matches": ["*://*.google.com/*", "*://*.nytimes.com/*"]

You can also exclude certain URLs using exclude_matches:

"exclude_matches": ["*://*.nytimes.com/business/*"]

Glob patterns let you specify more complex patterns for including and excluding URLs. Here's an example:

{
  "matches": ["*://*.example.com/*"],
  "exclude_matches": ["*://*.example.com/business/*"]
}

A More Complex Example: A Book Summarizing Extension

Let's now look at a more complex example. We'll build a Chrome extension that extracts book titles and authors from a web page, sends that information to the Gemini API to get summaries, and shows those summaries in an interactive popup when you click an icon next to the book title.

Let's start by configuring the extension manifest. Create a manifest.json file in your project directory and add the following configuration:

{
  "manifest_version": 3,
  "name": "Book Summary Extension",
  "version": "1.0",
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"]
    }
  ],
  "icons": {
    "16": "icon.png",
    "48": "icon.png",
    "128": "icon.png"
  },
  "web_accessible_resources": [
    {
      "resources": ["icon.png"],
      "matches": ["<all_urls>"]
    }
  ]
}

Now we create the background script (background.js). This script will handle the summary requests and the API responses. Create a background.js file and add the following code:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.bookTitle && message.bookAuthor) {
    let apiKey = 'YOUR_API_KEY';
    let apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`;

    let requestBody = {
      contents: [
        {
          role: 'user',
          parts: [
            {
              text: `Write a summary of the book "${message.bookTitle}" by "${message.bookAuthor}". Return your answer strictly as JSON following the provided schema.`
            }
          ]
        }
      ],
      generationConfig: {
        temperature: 1,
        topK: 64,
        topP: 0.95,
        maxOutputTokens: 8192,
        responseMimeType: 'application/json',
        responseSchema: {
          type: 'object',
          properties: {
            Title: { type: 'string' },
            Introduction: { type: 'string' },
            'Key Insights': {
              type: 'array',
              items: { type: 'string' }
            },
            'Book Summary': { type: 'string' },
            Conclusion: { type: 'string' }
          },
          required: ['Title', 'Introduction', 'Key Insights', 'Book Summary', 'Conclusion']
        }
      }
    };

    fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(requestBody)
    })
      .then(response => response.json())
      .then(data => {
        // The API returns an envelope; the generated text lives in candidates[0].
        let summaryText = data.candidates[0].content.parts[0].text;
        sendResponse({ summary: summaryText });
      })
      .catch(error => console.error('Error:', error));

    return true; // This indicates the response will be asynchronous
  }
});

There are several important details in this code worth understanding well:

  • We use the gemini-2.5-flash model. The Gemini 1.5 line (including gemini-1.5-flash) was retired from the API, so a tutorial using it today would fail.
  • We check message.bookTitle && message.bookAuthor. That's why, as we'll see in a moment, the content script must send the bookAuthor key, not author, or the condition will never be true and the summary will never be requested.
  • We ask Gemini to respond in JSON and set responseMimeType: 'application/json' along with a responseSchema. This is key, because later we'll run JSON.parse on the response; if the model returned free prose, that JSON.parse would throw.
  • Before responding we extract the actual text with data.candidates[0].content.parts[0].text. If we returned data as is, we'd be passing the full API envelope rather than the summary the rest of the code expects.

Security note: never ship a real API key inside an extension's code. Anyone can unpack the extension and read it. In production, put the key behind your own server or proxy that signs the requests, and keep 'YOUR_API_KEY' only as a placeholder while you test locally.

Let's continue with the content script (content-script.js). This script will extract the book titles and authors, send requests to the background script, and add an icon next to the book title. Notice that we now send bookAuthor (and not author) so it matches the background check:

chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) {
  if (request.extractBooks) {
    for (let i = 1; i <= 18; i++) {
      let selector = `body > app-root > div > app-home > div.sidebar-listado-libros.mt-2.mt-md-3.pt-md-3 > div > div > div:nth-child(2) > div.row.mx-0 > div > div > div.owl-stage-outer > div > div:nth-child(${i}) > div > div > div:nth-child(2) > a`;
      let authorSelector = `body > app-root > div > app-home > div.sidebar-listado-libros.mt-2.mt-md-3.pt-md-3 > div > div > div:nth-child(2) > div.row.mx-0 > div > div > div.owl-stage-outer > div > div:nth-child(${i}) > div > div > div.col-12.text--gray.text--xl.mb-2.px-0`;

      let element = document.querySelector(selector);
      let authorElement = document.querySelector(authorSelector);

      if (element && authorElement) {
        let bookTitle = element.innerText;
        let bookAuthor = authorElement.innerText;

        try {
          const response = await chrome.runtime.sendMessage({ bookTitle: bookTitle, bookAuthor: bookAuthor });

          if (response && response.summary) {
            // Add the icon next to the title
            let icon = document.createElement('img');
            icon.src = chrome.runtime.getURL('icon.png'); // Make sure you have an icon in your extension
            icon.style.cursor = 'pointer';
            icon.style.marginLeft = '10px';
            icon.style.width = '30px';
            icon.addEventListener('click', () => {
              showSummaryPopup(bookTitle, response.summary);
            });
            element.parentNode.insertBefore(icon, element.nextSibling);

            chrome.runtime.sendMessage({ bookTitle: bookTitle, bookAuthor: bookAuthor, summary: response.summary });
          }
        } catch (error) {
          console.error('Error fetching summary:', error);
        }
      }
    }
  }
});

function showSummaryPopup(title, summary) {
  const summaryData = JSON.parse(summary);

  // Create the floating window
  let popup = document.createElement('div');
  popup.style.position = 'fixed';
  popup.style.right = '10px';
  popup.style.bottom = '10px';
  popup.style.width = '400px';
  popup.style.maxHeight = '500px';
  popup.style.overflowY = 'auto';
  popup.style.backgroundColor = 'white';
  popup.style.border = '1px solid black';
  popup.style.padding = '15px';
  popup.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
  popup.style.zIndex = 1000;

  let titleElement = document.createElement('h2');
  titleElement.innerText = summaryData.Title;

  let introductionElement = document.createElement('p');
  introductionElement.innerHTML = `<strong>Introduction:</strong> ${summaryData.Introduction}`;

  let keyInsightsElement = document.createElement('div');
  keyInsightsElement.innerHTML = `<strong>Key Insights:</strong>`;
  summaryData["Key Insights"].forEach(insight => {
    let insightElement = document.createElement('p');
    insightElement.innerHTML = insight;
    keyInsightsElement.appendChild(insightElement);
  });

  let bookSummaryElement = document.createElement('p');
  bookSummaryElement.innerHTML = `<strong>Book Summary:</strong> ${summaryData["Book Summary"]}`;

  let conclusionElement = document.createElement('p');
  conclusionElement.innerHTML = `<strong>Conclusion:</strong> ${summaryData.Conclusion}`;

  let closeButton = document.createElement('button');
  closeButton.innerText = 'Close';
  closeButton.style.display = 'block';
  closeButton.style.margin = '10px auto';
  closeButton.addEventListener('click', () => {
    document.body.removeChild(popup);
  });

  popup.appendChild(titleElement);
  popup.appendChild(introductionElement);
  popup.appendChild(keyInsightsElement);
  popup.appendChild(bookSummaryElement);
  popup.appendChild(conclusionElement);
  popup.appendChild(closeButton);

  document.body.appendChild(popup);
}

Since showSummaryPopup runs JSON.parse(summary) and then reads summaryData.Title, summaryData["Key Insights"], and so on, that's exactly why in the background we asked Gemini to return structured JSON. If the model responded with plain text, this JSON.parse call would fail.

Now we create the extension popup. The popup.html and popup.js will let the user start the book extraction and will display the summaries obtained.

Create a popup.html file and add the following code:

<!DOCTYPE html>
<html>
<head>
  <title>Book Summary Extension</title>
  <script src="popup.js"></script>
  <style>
    #fetchSummaries {
      margin-bottom: 10px;
    }
    .summary-container {
      border: 1px solid #ccc;
      margin: 10px 0;
      padding: 10px;
      cursor: pointer;
    }
    .summary-container h2 {
      margin: 0;
      padding: 0;
    }
  </style>
</head>
<body>
  <h1>Book Summaries</h1>
  <button id="fetchSummaries">Fetch Summaries</button>
  <div id="summaries"></div>
</body>
</html>

Create a popup.js file and add the following code. As before, we read bookAuthor so the keys match across the whole extension:

document.getElementById('fetchSummaries').addEventListener('click', function () {
  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    chrome.tabs.sendMessage(tabs[0].id, { extractBooks: true });
  });
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.bookTitle && message.bookAuthor && message.summary) {
    let summaryData = JSON.parse(message.summary);

    let summaryContainer = document.createElement('div');
    summaryContainer.className = 'summary-container';

    let titleElement = document.createElement('h2');
    titleElement.innerText = summaryData.Title;
    titleElement.addEventListener('click', () => {
      toggleSummaryDetails(summaryContainer);
    });

    let detailsElement = document.createElement('div');
    detailsElement.style.display = 'none';

    let introductionElement = document.createElement('p');
    introductionElement.innerHTML = `<strong>Introduction:</strong> ${summaryData.Introduction}`;

    let keyInsightsElement = document.createElement('div');
    keyInsightsElement.innerHTML = `<strong>Key Insights:</strong>`;
    summaryData["Key Insights"].forEach(insight => {
      let insightElement = document.createElement('p');
      insightElement.innerHTML = insight;
      keyInsightsElement.appendChild(insightElement);
    });

    let bookSummaryElement = document.createElement('p');
    bookSummaryElement.innerHTML = `<strong>Book Summary:</strong> ${summaryData["Book Summary"]}`;

    let conclusionElement = document.createElement('p');
    conclusionElement.innerHTML = `<strong>Conclusion:</strong> ${summaryData.Conclusion}`;

    detailsElement.appendChild(introductionElement);
    detailsElement.appendChild(keyInsightsElement);
    detailsElement.appendChild(bookSummaryElement);
    detailsElement.appendChild(conclusionElement);

    summaryContainer.appendChild(titleElement);
    summaryContainer.appendChild(detailsElement);

    document.getElementById('summaries').appendChild(summaryContainer);
  }
});

function toggleSummaryDetails(summaryContainer) {
  let detailsElement = summaryContainer.querySelector('div');
  if (detailsElement.style.display === 'none') {
    detailsElement.style.display = 'block';
  } else {
    detailsElement.style.display = 'none';
  }
}

Finally, add the extension icon. Make sure you have an icon.png icon file in your project directory. This icon will be used to indicate that a summary is available next to the book title.

Everything is ready to test the extension. Follow these steps:

  1. Open Chrome and go to chrome://extensions/.
  2. Enable "Developer mode".
  3. Click "Load unpacked" and select your project directory.
  4. Open a web page compatible with the structure selected for the books.
  5. Click the extension icon and then "Fetch Summaries".

You should see the icons next to the book titles and, when you click them, a popup window showing the book summary.

Suggested Exercises

  1. Change the content script selectors so they target a real book page of your choice, and inspect the DOM to find the correct selectors.
  2. Add a loading state in the popup while waiting for the API response, so the user knows the summary is on its way.
  3. Move the API key out of the extension: create a small server or function that receives the title and author, calls Gemini with the key stored as an environment variable, and returns the summary.

Summary

We've built a Chrome extension that extracts book titles and authors from a web page, sends that information to the Gemini API to get summaries, and shows those summaries in an interactive popup. We covered the manifest configuration, the implementation of the background and content scripts, and the setup of the extension popup.

Content scripts are a powerful tool for modifying the behavior of web pages from a Chrome extension. By understanding how to inject these scripts, communicate with the background script, and customize their execution, you can build more effective and functional extensions. Keep exploring these techniques to get the most out of content scripts in your Chrome extension projects.

I hope this guide was useful and that you can apply it to a project you have in mind. Don't forget to leave me a comment if it helped or if you have any questions, and to subscribe for more content and keep improving your development skills.

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias