Como usuario de Google Chrome, probablemente hayas utilizado algunas extensiones en ese navegador. ¿Te has preguntado alguna vez cómo se construyen o si tú podrías construir una?

Este artículo te guía a través del proceso de creación de una extensión de Chrome, concretamente una que utiliza React y la API de Kinsta para gestionar plugins en sitios de WordPress alojados con Kinsta.

¿Qué es una extensión de Chrome?

Una extensión de Chrome es un programa que se instala en el navegador Chrome y mejora su funcionalidad. Las extensiones pueden ir desde simples botones con iconos en la barra de herramientas hasta funciones totalmente integradas que interactúan profundamente con tu experiencia de navegación.

Cómo crear una extensión de Chrome

Crear una extensión de Chrome es similar a desarrollar una aplicación web, pero requiere un archivo con formato JSON llamado manifest.json. Este archivo actúa como la columna vertebral de la extensión, dictando su configuración, permisos y funcionalidades que desees incluir.

Para empezar, crea una carpeta que contenga todos los archivos de tu extensión. A continuación, crea un archivo manifest.json en la carpeta.

Un archivo manifest.json básico para una extensión de Chrome incluye propiedades clave que definen la configuración básica de la extensión. A continuación se muestra un ejemplo de archivo manifest.json que incluye los campos necesarios para que funcione:

{
  "manifest_version": 3,
  "name": "My Chrome extension",
  "version": "1.0",
  "description": "Here is a description for my Chrome extension."
}

Puedes cargarlo y probarlo como una extensión desempaquetada en Chrome. Ve a chrome://extensions en tu navegador y activa el modo Desarrollador, luego haz clic en el botón Load Unpacked (Cargar Descomprimida). Se abrirá un explorador de archivos y podrás seleccionar el directorio que creaste para tu extensión.

Carga una extensión de Chrome haciendo clic en Load unpacked (Cargar descomprimida) en modo desarrollador
Carga una extensión de Chrome haciendo clic en Load unpacked (Cargar descomprimida) en modo desarrollador.

Cuando hagas clic en el icono de la extensión, no ocurrirá nada porque no has creado una interfaz de usuario.

Crear una interfaz de usuario (popup) para tu extensión de Chrome

Como en cualquier aplicación web, la interfaz de usuario (IU) de tu extensión utiliza HTML para estructurar el contenido, CSS para darle estilo y JavaScript para añadir interactividad.

Vamos a crear una IU básica utilizando todos estos archivos. Empieza creando un archivo HTML (popup.html). Este archivo define la estructura de los elementos de tu IU, como el texto, los encabezados, las imágenes y los botones. Añade el siguiente código:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hello World</title>
        <link rel="stylesheet" href="popup.css" />
    </head>
    <body>
        <h1>Hello World!</h1>
        <p>My first Chrome Extension</p>
        <button> id="sayHello">Say Hello</button>
        <script> src="popup.js"></script>
    </body>
</html>

El código anterior crea un encabezado, un párrafo y un botón. Los archivos CSS y JavaScript también están enlazados. Ahora, añade algunos estilos en el archivo popup.css:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    background-color: aliceblue;
    padding: 20px;
}

A continuación, en el archivo popup.js, añade un escuchador de eventos al botón para que, cuando se haga clic en él, se muestre una alerta:

const sayHelloBtn = document.getElementById('sayHello');
sayHelloBtn.addEventListener('click', async () => {
    let tab = await chrome.tabs.query({ active: true });
    chrome.scripting.executeScript({
        target: { tabId: tab[0].id },
        function: () => alert('Hello from the extension!'),
    });
});

Este código JavaScript recupera la pestaña activa actual y utiliza el Chrome Scripting API para ejecutar un script que muestre una alerta con un mensaje de saludo cuando se haga clic en el botón Say Hello. Esto introduce interactividad básica a tu extensión de Chrome.

Con estos pasos, has configurado una sencilla interfaz de usuario emergente para tu extensión de Chrome que incluye texto, estilo y funcionalidad básicos.

Por último, tienes que habilitar el archivo emergente en el archivo manifest.json añadiendo algunos permisos:

{
    . . . ,
    "action": {
        "default_popup": "popup.html"
    },
    "permissions": [
        "scripting",
        "tabs"
    ],
    "host_permissions": [
        "http://*/*",
        "https://*/*"
    ]
}

En la configuración anterior, la clave default_popup especifica que popup.html será la IU por defecto cuando el usuario interactúe con la extensión. El array permissions incluye scripting y tabs, que son cruciales para que la extensión interactúe con las pestañas y utilice las funciones de scripting del navegador.

El array host_permissions especifica con qué sitios puede interactuar tu extensión. Los patrones http://*/* y https://*/* indican que tu extensión puede interactuar con todos los sitios web a los que se accede a través de los protocolos HTTP y HTTPS.

Con estos ajustes en tu archivo manifest.json, tu extensión de Chrome está correctamente configurada para mostrar una ventana emergente y ejecutar scripts.

Recarga tu extensión de Chrome

Con estos cambios efectuados en tu carpeta local, necesitas actualizar la carpeta descomprimida cargada en Chrome. Para ello, abre la página de extensiones de Chrome, busca tu extensión y haz clic en el icono de recargar.

Haz clic en el icono de actualización para recargar la extensión.
Haz clic en el icono de actualización para recargar la extensión.

A continuación, haz clic en el icono de la extensión y aparecerá una ventana emergente. Cuando hagas clic en el botón Say Hello, aparecerá una alerta.

Ahora tienes conocimientos básicos sobre cómo empezar a crear una extensión de Chrome. Hay más cosas que se pueden hacer. Puedes manipular la interfaz de usuario de tu sitio web, hacer solicitudes a la API, recuperar datos de URL para realizar operaciones específicas, y mucho más.

Cómo crear una extensión de Chrome con React

Como hemos mencionado antes, crear una extensión de Chrome es similar a crear una aplicación web. Puedes utilizar frameworks populares como React.

Para React, el archivo manifest.json se crea en la carpeta public. Esta carpeta se utiliza para los activos estáticos que no quieres que sean procesados por Webpack (o paquetes similares que React puede utilizar en herramientas como Create React App).

Cuando construyes tu aplicación React, el proceso de construcción copia todo el contenido de la carpeta public en la carpeta dist. A continuación te explicamos cómo crear una extensión de Chrome con React:

  1. Crea una nueva aplicación React. Puedes utilizar el entorno de desarrollo local Vite ejecutando el siguiente comando en tu terminal:
npm create vite@latest

A continuación, da un nombre a tu proyecto y selecciona React como framework. Una vez hecho esto, navega hasta la carpeta del proyecto e instala las dependencias:

cd <project-name>
npm install
  1. En la carpeta public de tu proyecto React, crea un archivo manifest.json. Añade las siguientes configuraciones:
{
    "manifest_version": 3,
    "name": "React Chrome extension",
    "description": "Chrome extension built with React",
    "version": "0.1.0",
    "action": {
        "default_popup": "index.html"
    },
    "permissions": [
        "tabs"
    ],
    "host_permissions": [
        "http://*/*",
        "https://*/*"
    ]
}

La configuración para una extensión de Chrome incluye un objeto action que establece index.html como la ventana emergente predeterminada cuando se hace clic en el icono de la extensión. Este es el archivo HTML estático que se genera cuando construyes tu aplicación React.

  1. Desarrolla la aplicación React. Siéntete libre de hacer peticiones a la API, darles el estilo que desees, utilizar React Hooks, etc.
  1. Cuando hayas terminado de construir la interfaz de usuario de la extensión, ejecuta el comando build en React (npm run build). Todos tus activos, incluyendo tu archivo manifest.json, el index.html generado por React y otros, se mueven a la carpeta dist o build.
  2. Por último, carga tu extensión en Chrome. Navega a chrome://extensions/ y vuelve a cargar tu extensión.

Crear una extensión de Chrome para gestionar los plugins de tu sitio con la API de Kinsta

Este es el aspecto que tendrá la extensión de Chrome que vas a crear:

Extensión de Chrome construida con React interactuando con la API de Kinsta.
Extensión de Chrome construida con React interactuando con la API de Kinsta.

Al hacer clic, la extensión muestra una lista de sitios con plugins desactualizados en tu cuenta MyKinsta. Puedes ver una lista de los plugins y hacer clic en el botón Ver en My Kinsta para navegar a la página Temas y Plugins del sitio, donde puedes actualizar cada plugin.

Exploremos cómo crear la extensión de Chrome.

Comprender la API de Kinsta

La API de Kinsta es una potente herramienta que te permite interactuar mediante programación con los servicios de Kinsta, como los sitios de WordPress alojados. Puede ayudar a automatizar varias tareas relacionadas con la gestión de WordPress, incluyendo la creación de sitios, la recuperación de información del sitio, la obtención del estado de un sitio, la búsqueda y restauración de copias de seguridad, y mucho más.

Para utilizar la API de Kinsta, debes tener una cuenta con al menos un sitio de WordPress, una aplicación o una base de datos en MyKinsta. También debes generar una clave API para autenticarte y acceder a tu cuenta.

Para generar una clave API:

  1. Ve a tu panel de MyKinsta.
  2. Ve a la página Claves API (Tu nombre > Configuración de la empresa > Claves API).
  3. Haz clic en Crear Clave API.
  4. Elige una fecha de caducidad o establece una fecha de inicio personalizada y un número de horas para que caduque la clave.
  5. Dale a la clave un nombre único.
  6. Haz clic en Generar.

Después de crear una clave API, cópiala y guárdala en un lugar seguro (se recomienda utilizar un gestor de contraseñas). Puedes generar varias claves API, que aparecerán en la página Claves API. Si necesitas revocar una clave API, haz clic en el botón Revocar.

Gestionar los plugins de tu sitio con la API de Kinsta y React

Empecemos desarrollando una interfaz de usuario en React, que luego se transformará en una extensión de Chrome. Esta guía asume una familiaridad básica con React y la interacción con la API.

Configurar el entorno

En primer lugar, en el archivo App.jsx, define una constante para la URL de la API de Kinsta para evitar redundancias en tu código:

const KinstaAPIUrl = 'https://api.kinsta.com/v2';

Por seguridad, almacena los datos sensibles como tu clave API y el ID de la empresa Kinsta en un archivo .env.local para mantenerlos seguros y fuera de tu código fuente:

VITE_KINSTA_COMPANY_ID=YOUR_COMPANY_ID
VITE_KINSTA_API_KEY=YOUR_API_KEY

Obtener datos con la API de Kinsta

En el archivo App.jsx, tienes que hacer varias peticiones a la API de Kinsta para recuperar información sobre los sitios y sus plugins.

  1. Recuperar los sitios de la empresa: Empieza por obtener una lista de los sitios asociados a tu cuenta de empresa Kinsta. Utiliza el ID de la empresa en una solicitud GET, que devuelve un array con los detalles de los sitios.
    const getListOfCompanySites = async () => {
          const query = new URLSearchParams({
            company: import.meta.env.VITE_KINSTA_COMPANY_ID,
          }).toString();
          const resp = await fetch(`${KinstaAPIUrl}/sites?${query}`, {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          });
          const data = await resp.json();
          const companySites = data.company.sites;
          return companySites;
        }
  2. Obtén los datos del entorno de cada sitio: Para cada sitio, recupera los entornos, que incluyen el ID de entorno necesario para posteriores solicitudes. Esto implica mapear cada sitio y hacer una llamada a la API del endpoint /sites/${siteId}/environments.
     const companySites = await getListOfCompanySites();
        // Get all environments for each site
    
        const sitesEnvironmentData = companySites.map(async (site) => {
          const siteId = site.id;
          const resp = await fetch(`${KinstaAPIUrl}/sites/${siteId}/environments`, {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          });
          const data = await resp.json();
          const environments = data.site.environments;
          return {
            id: siteId,
            name: site.display_name,
            environments: environments,
          };
        });
  3. Recupera los plugins para cada entorno de sitio: Por último, utiliza el ID del entorno para obtener los plugins de cada sitio. Este paso implica una función de mapeo y una llamada a la API del endpoint /sites/environments/${environmentId}/plugins para cada entorno.
    // Wait for all the promises to resolve
        const sitesData = await Promise.all(sitesEnvironmentData);
    
        // Get all plugins for each environment
        const sitesWithPlugin = sitesData.map(async (site) => {
          const environmentId = site.environments[0].id;
          const resp = await fetch(
            `${KinstaAPIUrl}/sites/environments/${environmentId}/plugins`,
            {
              method: 'GET',
              headers: {
                Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
              },
            }
          );
          const data = await resp.json();
          const plugins = data.environment.container_info;
          return {
            env_id: environmentId,
            name: site.name,
            site_id: site.id,
            plugins: plugins,
          };
        });

    Ahora puedes reunir todas estas peticiones en una función que se utiliza para devolver el array final de sitios con detalles básicos sobre cada sitio y sus plugins:

    const getSitesWithPluginData = async () => {
      const getListOfCompanySites = async () => {
        const query = new URLSearchParams({
          company: import.meta.env.VITE_KINSTA_COMPANY_ID,
        }).toString();
        const resp = await fetch(`${KinstaAPIUrl}/sites?${query}`, {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
          },
        });
        const data = await resp.json();
        const companySites = data.company.sites;
        return companySites;
      }
    
      const companySites = await getListOfCompanySites();
    
      // Get all environments for each site
      const sitesEnvironmentData = companySites.map(async (site) => {
        const siteId = site.id;
        const resp = await fetch(`${KinstaAPIUrl}/sites/${siteId}/environments`, {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
          },
        });
        const data = await resp.json();
        const environments = data.site.environments;
        return {
          id: siteId,
          name: site.display_name,
          environments: environments,
        };
      });
    
      // Wait for all the promises to resolve
      const sitesData = await Promise.all(sitesEnvironmentData);
    
      // Get all plugins for each environment
      const sitesWithPlugin = sitesData.map(async (site) => {
        const environmentId = site.environments[0].id;
        const resp = await fetch(
          `${KinstaAPIUrl}/sites/environments/${environmentId}/plugins`,
          {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          }
        );
        const data = await resp.json();
        const plugins = data.environment.container_info;
        return {
          env_id: environmentId,
          name: site.name,
          site_id: site.id,
          plugins: plugins,
        };
      });
    
      // Wait for all the promises to resolve
      const sitesWithPluginData = await Promise.all(sitesWithPlugin);
      return sitesWithPluginData;
    }

Visualizar los datos de los sitios

Crea un estado con el hook useState para almacenar sitios con plugin(s) obsoletos. El hook useEffect también llamará al método getSitesWithPluginData() y extraerá los detalles del sitio cuando se monte el componente.

En el hook useEffect, crea una función que haga un bucle a través de cada sitio para filtrar los sitios con plugins obsoletos y luego almacenarlos en el estado:

const [sitesWithOutdatedPlugin, setSitesWithOutdatedPlugin] = useState([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  const checkSitesWithPluginUpdate = async () => {
    const sitesWithPluginData = await getSitesWithPluginData();
    const sitesWithOutdatedPlugin = sitesWithPluginData.map((site) => {
      const plugins = site.plugins.wp_plugins.data;
      const outdatedPlugins = plugins.filter((plugin) => plugin.update === "available");
      if (outdatedPlugins.length > 0) {
        const kinstaDashboardPluginPageURL = `https://my.kinsta.com/sites/plugins/${site.site_id}/${site.env_id}?idCompany=${import.meta.env.VITE_KINSTA_COMPANY_ID}`;
        return {
          name: site.name,
          plugins: outdatedPlugins,
          url: kinstaDashboardPluginPageURL,
        };
      }
    });

    setSitesWithOutdatedPlugin(sitesWithOutdatedPlugin);

  checkSitesWithPluginUpdate();
  setIsLoading(false);
}, []);

En el código anterior, observa que el estado de carga también se crea y se establece en true por defecto. Se utilizará para controlar cómo se muestran los datos. Cuando se hayan cargado todos los datos, lo establecemos en false.

A continuación se muestra un marcado para mostrar los datos del sitio y los plugins dentro de tu interfaz de usuario.

import { useEffect, useState } from "react"
import KinstaLogo from './assets/kinsta-logo.png'
import PluginPage from './components/PluginsPage'

function App() {
  // load the data from the API
  return (
    <div className="container">
        <div>
          <div> className="title-section">
            <img src={KinstaLogo} className="logo" alt="" />
          </div>
          <p> className="info-box">
            Get quick information about your site plugins that need update.
          </p>
          {isLoading ? (
            <p>Loading...</p>
          ) : (
            <>
              <div className="content">
                <p>The following sites have plugins that need to be updated.</p>
                {sitesWithOutdatedPlugin.map((site, index) => {
                  return (
                    <PluginPage key={index} {...site} />
                  );
                })}
              </div>
            </>
          )}
        </div>
    </div>
  )
}
export default App

El código incluye una cabecera con un logotipo y un párrafo informativo. El contenido de la interfaz de usuario se muestra condicionalmente en función del estado de isLoading. Si los datos aún se están cargando, muestra un mensaje de carga. Una vez cargados los datos, presenta los datos sobre los sitios y los plugins que requieren actualizaciones.

También verás un componente: PluginPage (PluginPage.jsx). Este componente está diseñado para mostrar sitios individuales y los detalles de sus plugins. Incluye una función para cambiar la visibilidad de los detalles de los plugins.

import { useState } from "react"
import { FaRegEye } from "react-icons/fa";
import { FaRegEyeSlash } from "react-icons/fa";

const PluginUse = (site) => {
    const [viewPlugin, setViewPlugin] = useState(false);

    return (
        <>
            <div className="site-card">
                <div className="site-card-details">
                    <p>{site.name}</p>
                    <div className="both-btns">
                        <a> href={site.url} target="_blank" rel="noreferrer" className="btn">
                            View in MyKinsta
                        </a>
                        <button onClick={() => setViewPlugin(!viewPlugin)} className="btn" title="View Plugins">
                            {viewPlugin ? <FaRegEyeSlash /> : <FaRegEye />}
                        </button>
                    </div>
                </div>
                {viewPlugin && (
                    <div className="plugin-list">
                        {site.plugins.map((plugin, index) => {
                            return (
                                <div key={index} className="plugin-card">
                                    <p>{plugin.name}</p>
                                    <div className="plugin-version-info">
                                        <p>Current Version: {plugin.version}</p>
                                        <p>Latest Version: {plugin.update_version}</p>
                                    </div>
                                </div>
                            );
                        })}
                    </div>
                )}
            </div>
        </>
    )
}
export default PluginUse

Configurar el archivo de manifiesto

Para transformar tu interfaz de usuario y funcionalidad en una extensión de Chrome, necesitas configurar el archivo manifest.json.

Crea un archivo manifest.json en la carpeta public y pega el código que aparece a continuación:

{
    "manifest_version": 3,
    "name": "Kinsta Plugins Manager - Thanks to Kinsta API",
    "description": "This extension allows you to manage your WordPress site's plugin from Kinsta's MyKinsta dashboard via Kinsta API.",
    "version": "0.1.0",
    "icons": {
        "48": "kinsta-icon.png"
    },
    "action": {
        "default_popup": "index.html"
    },
    "permissions": [
        "tabs"
    ],
    "host_permissions": [
        "https://my.kinsta.com/*"
    ]
}

Asegúrate de añadir el archivo del icono a tu carpeta public.

En este punto, ya puedes ejecutar el comando build (npm run build) para que todos tus activos, incluido tu archivo manifest.json, el index.html generado por React y otros archivos, se muevan a la carpeta dist o build.

A continuación, ve a chrome://extensions/ y cárgalo como una extensión descomprimida en Chrome. Haz clic en el botón Load Unpacked (Cargar Descomprimida) y selecciona el directorio que has creado para tu extensión.

Restringir la extensión a sitios específicos

Te habrás dado cuenta de que esta extensión funciona en cualquier momento. Queremos que sólo funcione cuando un usuario navegue al panel de MyKinsta.

Para ello, vamos a ajustar el archivo App.jsx. Crea un estado para almacenar la pestaña activa:

const [activeTab, setActiveTab] = useState(null);

A continuación, actualiza el Hook useEffect para definir e invocar la función getCurrentTab:

const getCurrentTab = async () => {
  const queryOptions = { active: true, currentWindow: true };
  const [tab] = await chrome.tabs.query(queryOptions);
  setActiveTab(tab);
}
getCurrentTab();

El código anterior utiliza chrome.tabs.query con opciones de consulta específicas para garantizar que sólo recupera la pestaña activa en la ventana actual. Una vez recuperada la pestaña, se establece como pestaña activa dentro del estado de la extensión.

Por último, implementa una lógica de representación condicional en la declaración de retorno de tu componente. Esto garantiza que la interfaz de usuario de gestión del plugin sólo aparezca cuando el usuario se encuentre en el panel de control de MyKinsta:

return (
  <div className="container">
    {activeTab?.url.includes('my.kinsta.com') ? (
      <div >
        <div className="title-section">
          <img src={KinstaLogo} className="logo" alt="" />
        </div>
        <p className="info-box">
          Get quick information about your site plugins that need update.
        </p>
        {isLoading ? (
          <p>Loading...</p>
        ) : (
          <>
            <div className="content">
              <p>The following {sitesWithPluginUpdate} sites have plugins that need to be updated.</p>
              {sitesWithOutdatedPlugin.map((site, index) => {
                return (
                  <PluginPage key={index} {...site} />
                );
              })}
            </div >
          </>
        )}
      </div >
    ) : (
      <div >
        <div className="title-section">
          <img src={KinstaLogo} className="logo" alt="" />
        </div>
        <p className="info-box">
          This extension is only available on Kinsta Dashboard.
        </p>
      </div>
    )}
  </div>
)

Tras realizar los cambios, reconstruye tu aplicación y vuelve a cargar la extensión de Chrome. Esto aplicará la nueva lógica y las restricciones.

Resumen

En este artículo, has aprendido los fundamentos de la creación de una extensión de Chrome y cómo crear una con React. También has aprendido a crear una extensión que interactúa con la API de Kinsta.

Como usuario de Kinsta, puedes aprovechar el enorme potencial y flexibilidad que aporta la API de Kinsta, ya que te ayuda a desarrollar soluciones personalizadas para gestionar tus sitios, aplicaciones y bases de datos.

¿Qué endpoint de la API de Kinsta has utilizado mucho y cómo lo has usado? ¡Compártelo con nosotros en la sección de comentarios!

Joel Olawanle Kinsta

Joel is a Frontend developer working at Kinsta as a Technical Editor. He is a passionate teacher with love for open source and has written over 200 technical articles majorly around JavaScript and it's frameworks.