Cómo Utilizar Content Scripts en Extensiones para Chrome
Jul 19, 2024
Updated: Jun 24, 2026

Cómo Utilizar Content Scripts en Extensiones para Chrome

En este post veremos cómo trabajar con content scripts en extensiones para Chrome. Los content scripts son archivos que se ejecutan en el contexto de una página web y permiten modificar su contenido. Vamos a explorar cómo inyectar estos scripts, comunicarnos con el background script y personalizar su ejecución en diferentes sitios web.

¿Qué son los Content Scripts?

Los content scripts son archivos de JavaScript que se ejecutan en el contexto de la página web, permitiendo manipular el DOM de la página en la que están inyectados. Estos scripts viven en un mundo aislado, lo que significa que pueden interactuar con la página sin interferir con otros scripts que la página pueda estar ejecutando.

Configuración básica de Content Scripts

Lo primero es inyectar nuestro content script. Para ello necesitas configurar tu manifest.json e incluir los content scripts. Aquí tienes un ejemplo:

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

Este archivo especifica que el content-script.js se inyectará en todas las páginas de Google.

Ahora crea un archivo llamado content-script.js con el siguiente contenido:

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

Para verificar la inyección, carga tu extensión en Chrome y navega a google.com. Deberías ver el mensaje de alerta aparecer, indicando que el content script se ha inyectado correctamente.

Comunicación entre Content Scripts y Background Scripts

Para realizar tareas más avanzadas, los content scripts deben comunicarse con el background script. Vamos a ver cómo enviar y recibir mensajes entre ellos.

Primero, enviamos un mensaje desde el content script. Modifica tu content-script.js para enviar un mensaje al background script:

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

Luego, recibimos el mensaje en el background script. Modifica tu background.js para escuchar el mensaje y responder:

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

Para probar la comunicación, recarga la extensión y verifica la consola de la página de Google para ver el mensaje de respuesta.

Personalización de Content Scripts

Puedes personalizar cuándo y dónde se ejecutan los content scripts usando patrones de coincidencia y exclusión.

Con los patrones de coincidencia puedes especificar en qué URLs se deben inyectar los content scripts usando el atributo matches. Aquí tienes un ejemplo para inyectar en múltiples sitios:

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

También puedes excluir ciertas URLs usando exclude_matches:

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

Los glob patterns te permiten especificar patrones más complejos para la inclusión y exclusión de URLs. Aquí tienes un ejemplo:

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

Un ejemplo más complejo: extensión para resumir libros

Veamos ahora un ejemplo más complejo. Vamos a crear una extensión de Chrome que extrae títulos y autores de libros de una página web, envía esta información a la API de Gemini para obtener resúmenes, y muestra estos resúmenes en un popup interactivo al hacer clic en un icono junto al título del libro.

Empecemos por configurar el manifiesto de la extensión. Crea un archivo manifest.json en el directorio de tu proyecto y añade la siguiente configuración:

{
  "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>"]
    }
  ]
}

Ahora creamos el script de fondo (background.js). Este script manejará las solicitudes de resúmenes y las respuestas de la API. Crea un archivo background.js y añade el siguiente código:

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 => {
        // La API devuelve un sobre; el texto generado vive en candidates[0].
        let summaryText = data.candidates[0].content.parts[0].text;
        sendResponse({ summary: summaryText });
      })
      .catch(error => console.error('Error:', error));

    return true; // Esto indica que la respuesta será asíncrona
  }
});

Hay varios detalles importantes en este código que conviene entender bien:

  • Usamos el modelo gemini-2.5-flash. La línea Gemini 1.5 (incluido gemini-1.5-flash) fue retirada de la API, así que un tutorial que la usara hoy fallaría.
  • Comprobamos message.bookTitle && message.bookAuthor. Por eso, como veremos enseguida, el content script debe enviar la clave bookAuthor, no author, o la condición nunca será verdadera y el resumen no se solicitará.
  • Pedimos a Gemini que responda en JSON y fijamos responseMimeType: 'application/json' junto con un responseSchema. Esto es clave, porque más adelante haremos JSON.parse sobre la respuesta; si el modelo devolviera prosa libre, ese JSON.parse lanzaría una excepción.
  • Antes de responder extraemos el texto real con data.candidates[0].content.parts[0].text. Si devolviéramos data tal cual, estaríamos pasando el sobre completo de la API y no el resumen que espera el resto del código.

Nota de seguridad: nunca empaquetes una API key real dentro del código de una extensión. Cualquiera puede descomprimir la extensión y leerla. En producción, coloca la clave detrás de un servidor o proxy propio que firme las solicitudes, y deja 'YOUR_API_KEY' solo como marcador de posición mientras pruebas en local.

Sigamos con el script de contenido (content-script.js). Este script extraerá los títulos y autores de los libros, enviará solicitudes al background script y añadirá un icono junto al título del libro. Fíjate en que ahora enviamos bookAuthor (y no author) para que coincida con la comprobación del background:

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) {
            // Añadir el icono junto al título
            let icon = document.createElement('img');
            icon.src = chrome.runtime.getURL('icon.png'); // Asegúrate de tener un icono en tu extensión
            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);

  // Crear la ventana flotante
  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);
}

Como showSummaryPopup hace JSON.parse(summary) y luego lee summaryData.Title, summaryData["Key Insights"], etc., es justamente por eso que en el background le pedimos a Gemini que devuelva JSON estructurado. Si el modelo respondiera texto plano, esta llamada a JSON.parse fallaría.

Ahora creamos el popup de la extensión. El popup.html y el popup.js permitirán al usuario iniciar la extracción de libros y mostrarán los resúmenes obtenidos.

Crea un archivo popup.html y añade el siguiente código:

<!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>

Crea un archivo popup.js y añade el siguiente código. Igual que antes, leemos bookAuthor para que las claves coincidan en toda la extensión:

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';
  }
}

Por último, añade el icono de la extensión. Asegúrate de tener un archivo de icono icon.png en el directorio de tu proyecto. Este icono se utilizará para indicar que hay un resumen disponible junto al título del libro.

Ya está todo listo para probar la extensión. Sigue estos pasos:

  1. Abre Chrome y ve a chrome://extensions/.
  2. Activa el "Modo de desarrollador".
  3. Haz clic en "Cargar descomprimida" y selecciona el directorio de tu proyecto.
  4. Abre una página web compatible con la estructura seleccionada para los libros.
  5. Haz clic en el icono de la extensión y luego en "Fetch Summaries".

Deberías ver los iconos junto a los títulos de los libros y, al hacer clic en ellos, una ventana emergente mostrando el resumen del libro.

Ejercicios propuestos

  1. Cambia los selectores del content script para que apunten a una página de libros real que tú elijas, e inspecciona el DOM para encontrar los selectores correctos.
  2. Añade un estado de carga en el popup mientras se espera la respuesta de la API, para que el usuario sepa que el resumen está en camino.
  3. Mueve la API key fuera de la extensión: crea un pequeño servidor o función que reciba el título y el autor, llame a Gemini con la clave guardada como variable de entorno, y devuelva el resumen.

Resumen

Hemos creado una extensión de Chrome que extrae títulos y autores de libros de una página web, envía esta información a la API de Gemini para obtener resúmenes, y muestra estos resúmenes en un popup interactivo. Hemos cubierto la configuración del manifiesto, la implementación de los scripts de fondo y de contenido, y la configuración del popup de la extensión.

Los content scripts son una herramienta poderosa para modificar el comportamiento de las páginas web desde una extensión para Chrome. Al entender cómo inyectar estos scripts, comunicarse con el background script y personalizar su ejecución, puedes crear extensiones más efectivas y funcionales. Sigue explorando estas técnicas para aprovechar al máximo las capacidades de los content scripts en tus proyectos de extensión para Chrome.

Espero que esta guía te haya sido útil y la puedas aplicar a algún proyecto que tengas en mente. No olvides dejarme un comentario si te sirvió o si tienes alguna duda, y suscribirte para más contenido y seguir mejorando tus habilidades de desarrollo.

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias