As a Google Chrome user, you’ve probably used some extensions in that browser. Have you ever wondered how they are built or if you could build one?

This article guides you through the process of creating a Chrome extension, specifically one that uses React and the Kinsta API to manage plugins on WordPress sites hosted with Kinsta.

What is a Chrome extension?

A Chrome extension is a program installed in the Chrome browser and enhances its functionality. Extensions can range from simple icon buttons in the toolbar to fully integrated features that interact deeply with your browsing experience.

How to create a Chrome extension

Creating a Chrome extension is similar to developing a web application, but it requires a JSON-formatted file called manifest.json. This file acts as the backbone of the extension, dictating its settings, permissions, and functionalities you wish to include.

To start, create a folder that will hold all your extension files. Next, create a manifest.json file in the folder.

A basic manifest.json file for a Chrome extension includes key properties that define the extension’s basic settings. Below is an example of a manifest.json file that includes the necessary fields to make it work:

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

You can load and test this as an unpacked extension to Chrome. Navigate to chrome://extensions in your browser and toggle Developer mode, then click the Load Unpacked button. This will open a file browser, and you can select the directory you created for your extension.

Load a Chrome extension by clicking Load unpacked in developer mode
Load a Chrome extension by clicking Load unpacked in developer mode.

When you click the extension icon, nothing will happen because you have not created a user interface.

Create a user interface (popup) for your Chrome extension

Like with every web application, the user interface (UI) of your extension uses HTML to structure the content, CSS to style it, and JavaScript to add interactivity.

Let’s create a basic UI using all of these files. Start by creating an HTML file (popup.html). This file defines the structure of your UI elements, such as text, headings, images, and buttons. Add the following code:

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

The code above creates a heading, paragraph, and button. The CSS and JavaScript files are also linked. Now, add some styles in the popup.css file:

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

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

Next, in the popup.js file, add an event listener to the button so that when it is clicked, an alert is displayed:

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!'),
    });
});

This JavaScript code retrieves the current active tab and uses the Chrome Scripting API to execute a script that displays an alert with a greeting message when the Say Hello button is clicked. This introduces basic interactivity to your Chrome extension.

With these steps, you have set up a simple popup UI for your Chrome extension that includes basic text, styling, and functionality.

Finally, you need to enable the popup file in the manifest.json file by adding some permissions:

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

In the configuration above, the default_popup key specifies that popup.html will be the default UI when the user interacts with the extension. The permissions array includes scripting and tabs, which are crucial for the extension to interact with the tabs and use the browser’s scripting features.

The host_permissions array specifies which sites your extension can interact with. The patterns http://*/* and https://*/* indicate that your extension can interact with all websites accessed over HTTP and HTTPS protocols.

With these settings in your manifest.json file, your Chrome extension is properly configured to display a popup and execute scripts.

Reload your Chrome extension

With these changes effected in your local folder, you need to update the unpacked folder loaded to Chrome. To do this, open the Chrome extensions page, find your extension, and click the reload icon.

Click the refresh icon to reload the extension
Click the refresh icon to reload the extension.

You can then click the extension icon, and a popup will appear. When you click the Say Hello button, an alert will appear.

You now have a basic knowledge of how to begin building a Chrome extension. There is more that can be done. You can manipulate your site’s UI, make API requests, retrieve data from URLs to perform specific operations, and more.

How to create a Chrome extension with React

As we mentioned earlier, creating a Chrome extension is similar to building a web application. You can use popular web frameworks like React.

For React, the manifest.json file is created in the public folder. This folder is used for static assets you do not want to be processed by Webpack (or similar bundlers that React might use under the hood in tools like Create React App).

When you build your React application, the build process copies all contents of the public folder into the dist folder. Here is how to create a Chrome extension with React:

  1. Create a new React application. You can use the local development environment Vite by running the following command in your terminal:
npm create vite@latest

Next, give your project a name and select React as the framework. Once this is done, navigate into the project folder and install the dependencies:

cd <project-name>
npm install
  1. In your React project’s public folder, create a manifest.json file. Add the following configurations:
{
    "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://*/*"
    ]
}

The configuration for a Chrome extension includes an action object that sets index.html as the default popup when the extension icon is clicked. This is the static HTML file generated when you build your React application.

  1. Develop the React application. Feel free to make API requests, style them as you wish, use React Hooks, and more.
  1. When you are done building the UI of the extension, run the build command in React (npm run build). All your assets, including your manifest.json file, React-generated index.html, and others, are moved into the dist or build folder.
  2. Finally, load your extension into Chrome. Navigate to chrome://extensions/ and reload your extension.

Creating a Chrome extension to manage your site’s plugins with Kinsta API

This is what the Chrome extension you’ll build will look like:

Chrome extension built with React interacting with the Kinsta API
Chrome extension built with React interacting with the Kinsta API.

When clicked, the extension displays a list of sites with outdated plugins on your MyKinsta account. You can see a list of the plugins and click the View in MyKinsta button to navigate to the site’s Themes & Plugins page, where you can update each plugin.

Let’s explore how to create the Chrome extension.

Understanding the Kinsta API

The Kinsta API is a powerful tool that allows you to interact programmatically with Kinsta services like hosted WordPress sites. It can help automate various tasks related to WordPress management, including site creation, retrieving site information, getting a site’s status, browsing and restoring backups, and more.

To use Kinsta’s API, you must have an account with at least one WordPress site, application, or database in MyKinsta. You must also generate an API key to authenticate and access your account.

To generate an API key:

  1. Go to your MyKinsta dashboard.
  2. Navigate to the API Keys page (Your name > Company settings > API Keys).
  3. Click Create API Key.
  4. Choose an expiration or set a custom start date and number of hours for the key to expire.
  5. Give the key a unique name.
  6. Click Generate.

After creating an API key, copy it and store it somewhere safe (using a password manager is recommended). You can generate multiple API keys, which will be listed on the API Keys page. If you need to revoke an API key, click the Revoke button.

Manage your site’s plugins with Kinsta API and React

Let’s start by developing a user interface in React, which will then be transformed into a Chrome extension. This guide assumes basic familiarity with React and API interaction.

Setting up the environment

Firstly, in the App.jsx file, define a constant for the Kinsta API URL to avoid redundancy in your code:

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

For security, store sensitive data such as your API key and Kinsta company ID in a .env.local file to keep them secure and out of your source code:

VITE_KINSTA_COMPANY_ID=YOUR_COMPANY_ID
VITE_KINSTA_API_KEY=YOUR_API_KEY

Fetch data with Kinsta API

In the App.jsx file, you need to make several requests to the Kinsta API to retrieve information about sites and their plugins.

  1. Retrieve company sites: Begin by fetching a list of sites associated with your Kinsta company account. Use the company ID in a GET request, which returns an array of site details.
    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. Fetch environment data for each site: For each site, retrieve the environments, which include the environment ID necessary for further requests. This involves mapping over each site and making an API call to the /sites/${siteId}/environments endpoint.
     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. Retrieve plugins for each site environment: Finally, use the environment ID to fetch plugins for each site. This step involves a mapping function and an API call to the /sites/environments/${environmentId}/plugins endpoint for each environment.
    // 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,
          };
        });

    You can now put all of these requests together into a function that is used to return the final array of sites with basic details about each site and its 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;
    }

Displaying site data

Create a state with the useState hook to store sites with outdated plugin(s). The useEffect hook will also call the getSitesWithPluginData() method and extract the site details when the component is mounted.

In the useEffect hook, create a function that will loop through each site to filter out sites with outdated plugins and then store them in the state:

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);
}, []);

In the code above, you notice that the loading state is also created and set to true by default. This will be used to control how data is displayed. When all the data is loaded, we set it to false.

Below is a markup to render the site data and plugins within your UI.

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

The code includes a header with a logo and an informational paragraph. The content of the UI is conditionally rendered based on the isLoading state. If data is still loading, it displays a loading message. Once data is loaded, it presents the data about the sites and any plugins requiring updates.

You will also notice a component: PluginPage (PluginPage.jsx). This component is designed to display individual sites and their plugin details. It includes a functionality to toggle the visibility of the plugin details.

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

Configure the manifest file

To transform your user interface and functionality into a Chrome extension, you need to configure the manifest.json file.

Create a manifest.json file in the public folder and paste the code below:

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

Be sure to add the icon file to your public folder.

At this point, you can now run the build command (npm run build) so all your assets, including your manifest.json file, React-generated index.html, and other files, are moved into the dist or build folder.

Next, navigate to chrome://extensions/ and load this as an unpacked extension to Chrome. Click the Load Unpacked button and select the directory you created for your extension.

Restrict extension to specific sites

You notice that this extension works at any time. We want it to work only when a user is navigated to the MyKinsta dashboard.

To do this, let’s adjust the App.jsx file. Create a state to store the active tab:

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

Next, update the useEffect Hook to define and invoke the getCurrentTab function:

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

The above code uses chrome.tabs.query with specific query options to ensure it retrieves only the active tab in the current window. Once the tab is retrieved, it’s set as the active tab within the extension’s state.

Finally, implement a conditional rendering logic in your component’s return statement. This ensures that the plugin management UI appears only when the user is on the MyKinsta dashboard:

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

After making the changes, rebuild your application and reload the Chrome extension. This will apply the new logic and restrictions.

Summary

In this article, you have learned the basics of creating a Chrome extension and how to create one with React. You’ve also learned how to create an extension that interacts with the Kinsta API.

As a Kinsta user, you can take advantage of the enormous potential and flexibility the Kinsta API brings as it helps you develop custom solutions to manage your sites, applications, and databases.

What endpoint of the Kinsta API have you been using a lot, and how have you used it? Share with us in the comment section!

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.