Build a Chrome Extension to Streamline Your Workflow
Tags: webdev, javascript, productivity, wordpress
Published: 2020-05-14 07:56:06

*By [James Hibbard](https://www.sitepoint.com/author/jhibbard). This article was originally published [on SitePoint](https://www.sitepoint.com/build-a-chrome-extension/).*

**When I began as a JavaScript editor at SitePoint, our submission process was something of a mess — articles coming from every direction in a variety of formats. So we decided to standardize things and settled on submission via GitHub in Markdown.**

This was a step forward, but we still needed to convert the Markdown into HTML for our WordPress back end. The powers that be at SitePoint HQ at the time had vetoed the installation of any WordPress plugins, which made us consider if we could accomplish this task with a browser extension. Luckily we could!

In the following post, I'm going to demonstrate how you can build your own Chrome extension to add additional functionality to WordPress. I'll also introduce you to [SP-Tools](https://github.com/jameshibbard/SP-Tools), the extension we use at SitePoint to make life as an editor that little bit easier.

## The Anatomy of a Chrome Extension

Despite what you might think, building a Chrome extension isn't difficult. Let's start by looking at the various components.

_Parts of the following section are borrowed from another tutorial I wrote about [building a Chrome extension using Vue.js](https://www.sitepoint.com/build-vue-chrome-extension/)._

The core piece of any Chrome extension is a [manifest file](https://developer.chrome.com/extensions/manifest). This is in a JSON format and provides important information about an extension, such as its version, resources, or the permissions it requires.

A manifest file won't do much on its own, so we can use a [content script](https://developer.chrome.com/extensions/content_scripts) to add some functionality. Content scripts are files that "run in the context of web pages". That is to say, you specify the URL in your manifest file, then when your browser visits a page whose address matches the URL you specified, the content script is injected into the page and run.

To demonstrate these concepts, let’s start by writing a Chrome extension to do something on the SitePoint main site.

Make a new folder called `my-extension` and two files, `manifest.json` and `main.js`:

```bash
mkdir my-extension
cd my-extension
touch manifest.json main.js
```

Open up `manifest.json` and add the following code:

```javascript
{
  "name": "My Extension",
  "version": "0.0.1",
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": [ "*://*.sitepoint.com/*" ],
      "js": [ "main.js" ]
    }
  ]
}
```

The `name`, `version` and `manifest_version` are all required fields. The `name` and `version` fields can be whatever you want; the [manifest version](https://developer.chrome.com/extensions/manifestVersion) should be set to 2 (as of Chrome 18).

The `content_scripts` key allows us to register a content script (`main.js`), which will be run whenever we visit SitePoint. Notice how we can use [match patterns](https://developer.chrome.com/extensions/match_patterns) to specify parts of the URL, such as the protocol.

Now let’s add the following code to `main.js` to make the browser say hello whenever we visit SitePoint:

```javascript
alert('Hello there!');
```

Finally, let’s install the extension. Open Chrome and enter `chrome://extensions/` in the address bar. You should see a page displaying the extensions you’ve installed.

As we want to install our extension from a file (and not the Chrome Web Store) we need to activate _Developer mode_ using the toggle in the top right-hand corner of the page. This should add an extra menu bar with the option _Load unpacked_. Click this button and select the `my-extension` folder you created previously. Click _Open_ and the extension will be installed.

Now when you visit SitePoint, this will happen:

![Basic Extension](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2020/03/158348751501-chrome-ext.png)

Congratulations! You just made a Chrome extension.

### Background Scripts and Message Passing

So, that dialogue box is pretty annoying right? To finish off this section, let's add a context menu entry to fire it off manually, instead of having it appear on every page load.

This introduces us to another important component of Chrome extensions — [background scripts](https://developer.chrome.com/extensions/background_pages). These scripts can react to browser events (such as a user clicking a context menu entry) and they have full access to Chrome's APIs. However, they _don’t_ have access to the current page, and rely on message passing to communicate with content scripts.

Update the manifest like so:

```javascript
{
  "name": "My Extension",
  "version": "0.0.1",
  "manifest_version": 2,
  "permissions": [ "contextMenus" ],
  "content_scripts": [
    {
      "matches": [ "*://*.sitepoint.com/*" ],
      "js": [ "main.js" ]
    }
  ],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  }
}
```

Notice that we’re requesting the `contextMenus` permission, as we want to add something to the context menu, and that we’ve registered a non-persistent background script. Making the background script non persistent allows it to be unloaded when it’s not needed.

Next, create a `background.js` file and add:

```javascript
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: 'greet',
    title: 'Say hi',
    contexts: ['page'],
    documentUrlPatterns: ['*://*.sitepoint.com/*'],
  });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'greet') {
    chrome.tabs.sendMessage(tab.id, { text: 'greet' }, (res) => {
      console.log(res);
    });
  }
});
```

We register the context menu entry when the extension is installed, then add an event listener to send a message to our content script whenever the entry is clicked.

Change `main.js` like so:

```javascript
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.text === 'greet') {
    alert('hi');
    sendResponse('I greeted the user');
  }
});
```

Here, we listen for a message from our background script. If it has a text of "greet", we then fire off the alert and send back a message informing the background script that we did what was requested.

To try this out, head back to the extensions page (at `chrome://extensions/`), then click the reload icon and reload any SitePoint page. When you right click, you should now see a context menu entry.

![Context menu entry for our extension](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2020/03/158349074502-chrome-ext.png)

## Enhancing WordPress' Functionality with a Chrome Extension

Now that we’ve familiarized ourselves with the basic components of a Chrome extension, let's look at how we can make some additions to WordPress' functionality.

To follow along with this section, you’ll need a [working installation of WordPress](https://wordpress.org/support/article/how-to-install-wordpress/). I installed mine locally. It’s running on an Apache server at `http://localhost/wp`.

The code for this extension can be found on [GitHub](https://github.com/sitepoint-editors/wp-enhance).

### Adding a Markdown Converter

Let's start by adding a Markdown converter to the WordPress editor. True to the experience on SitePoint, I'll be using the "classic" editor (achieved by installing the [Disable Gutenberg plugin](https://wordpress.org/plugins/disable-gutenberg/)) and the _Text_ view.

To begin, create the following folder structure for our new extension:

```bash
wp-enhance
├── lib
│   ├── jquery.min.js
│   └── showdown.min.js
├── manifest.json
└── scripts
    └── main.js
```

On the command line:

```bash
mkdir wp-enhance
cd wp-enhance
mkdir lib scripts
touch lib/showdown.min.js lib/jquery.min.js
touch scripts/main.js
touch manifest.json
```

As you can see, we’ll be using the [Showdown markdown converter](https://github.com/showdownjs/showdown) and jQuery (because I'm lazy).

The first order of business is to grab the latest minified version of these libraries ([Showdown](https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js) and [jQuery](https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js)) and add the contents to the appropriate files.

Next, add the following code to `manifest.json`:

```javascript
{
  "manifest_version": 2,
  "name": "WP Enhance",
  "description": "A Chrome extension to enhance WordPress' functionality",
  "version": "0.0.1",
  "content_scripts": [
    {
      "matches": [ "http://localhost/wp/wp-admin/post-new.php" ],
      "js": [
        "lib/jquery.min.js",
        "lib/showdown.min.js",
        "scripts/main.js"
      ]
    }
  ]
}
```

There's nothing spectacular going on here. The extension is set to run when we visit `http://localhost/wp/wp-admin/post-new.php`, and we're including the two libraries we just downloaded.

Finally, in `scripts/main` add the following:

```javascript
'use strict';

const $editorToolbar = $('#ed_toolbar');
const $mainTextArea = $('#content');

function getShowdownConverter() {
  const converter = new showdown.Converter();

  // Set any options here, for example to add table support
  converter.setOption('tables', true);

  return converter;
}

function addMDButton() {
  const mdConverter = getShowdownConverter();
  const $convertButton = $('<input />', {
    type: 'button',
    value: 'MD',
    class: 'ed_button button button-small',
    title: 'Convert MD to HTML',
    click() {
      const md = $mainTextArea.val();
      const html = mdConverter.makeHtml(md);
      $mainTextArea.val(html);
    },
  });
  $editorToolbar.append($convertButton);
}

addMDButton();
```

Here, we’re creating a new button and appending it to the WordPress editor's toolbar. When it’s clicked, we’re calling Showdown's `makeHtml` method, which we pass the contents of the content area. This returns us some HTML, which we then insert back into the editor.

Load the extension and visit the new post page. You should see something like this:

![The Markdown Converter](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2020/03/158349764203-chrome-ext.png)

I'm sure you'll agree, that's a reasonably impressive result in just a few lines of code.

### Adding a Date Picker to the Publish Widget

Next, we're going to enhance the publish widget with a datepicker. This will replace the series of drop-downs and input boxes that you normally see when you click the _Edit_ link next to the "Publish immediately" message in WordPress' _Publish_ widget.

The first thing we’ll need to do is to download a datepicker. For this demo I'll be using [this one](https://github.com/xdan/datetimepicker). You can download the necessary files from [here](https://github.com/xdan/datetimepicker/releases/tag/2.5.20). Unzip that file and place `build/jquery.datetimepicker.full.min.js` into our `lib` folder. Then create a new `css` folder in the extension and place `build/jquery.datetimepicker.min.css` into it.

Our extension should now look like this:

```bash
wp-enhance
├── css
│   └── jquery.datetimepicker.min.css
├── lib
│   ├── jquery.datetimepicker.full.min.js
│   ├── jquery.min.js
│   └── showdown.min.js
├── manifest.json
└── scripts
    └── main.js
```

Now include these files in the manifest:

```javascript
{
  "manifest_version": 2,
  "name": "WP Enhance",
  "description": "A Chrome extension to enhance WordPress' functionality",
  "version": "0.0.1",
  "content_scripts": [
    {
      "matches": [ "http://localhost/wp/wp-admin/post-new.php" ],
      "js": [
        "lib/jquery.min.js",
        "lib/showdown.min.js",
        "lib/jquery.datetimepicker.full.min.js",
        "scripts/main.js"
      ],
      "css": [ "css/jquery.datetimepicker.min.css" ]
    }
  ]
}
```

Finally, we need to alter our content script (`main.js`) to look like this:

```javascript
const $editorToolbar = $('#ed_toolbar');
const $mainTextArea = $('#content');
const $timeStampDiv = $('#timestampdiv');
const $wpSchedulePostDropdown = $('.timestamp-wrap');
let $datepicker;

const $dd = $('#jj');
const $mm = $('#mm');
const $yyyy = $('#aa');
const $hh = $('#hh');
const $mn = $('#mn');

function getShowdownConverter() { ... }

function addMDButton() { ... }

function addDatePicker() {
  $datepicker = $('<input />', {
    id: 'bandaid-datepicker',
    type: 'text',
    placeholder: 'Date and time',
  });

  $datepicker.datetimepicker();
  $timeStampDiv.prepend($datepicker);
}

addMDButton();

$wpSchedulePostDropdown.hide();
addDatePicker();

$datepicker.on('change', function updateDateFields() {
  // String in format yyyy/mm/dd hh:mm
  const dateString = this.value;

  $yyyy.val(dateString.substr(0, 4));
  $mm.val(dateString.substr(5, 2));
  $dd.val(dateString.substr(8, 2));
  $hh.val(dateString.substr(11, 2));
  $mn.val(dateString.substr(14, 2));
});
```

What we’re doing is getting a reference to the input elements that WP uses to manage the time and date of the scheduled post. We’re then hiding these elements and initializing the datepicker. Whenever a user selects a date, the hidden field is updated and the post can be scheduled.

Reload the extension, then refresh the WordPress new post page. What you have now should look like this:

![The datepicker widget](https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2020/03/158350271304-chrome-ext.png)

Again, an impressive result for not much code.

## Testing the Extension

One of the things I noticed early on with our SP-Tools extension was that, when WordPress got updated, things would break. So, I got to thinking how I could test the extension and decided that some end-to-end tests with [Nightwatch](https://nightwatchjs.org/) would make sense.

In the following section, I'll demonstrate how we can test our extension in the same way.

First, we'll need to generate a `package.json` file. In the extension root, run `npm init -y`. Next, let's install Nightwatch and the ChromeDriver as dev dependencies:

```bash
npm install --save-dev nightwatch chromedriver
```

Now make a `test`directory and add a `nightwatch.config.js` file, as well as a `wp.js` file for our test code:

```bash
mkdir test
touch test/nightwatch.config.js test/wp.js
```

Add the following to the config file:

```javascript
module.exports = {
  src_folders: 'test',
  output_folder: 'test',
  page_objects_path: '',
  custom_commands_path: '',
  custom_assertions_path: '',

  webdriver: {
    start_process: true,
    port: 9515,
    server_path: 'node_modules/.bin/chromedriver',
    log_path: false,
    cli_args: [],
  },

  test_settings: {
    default: {
      desiredCapabilities: {
        browserName: 'chrome',
        chromeOptions: {
          args: [
            'load-extension=./',
            '--test-type',
          ],
        },
      },
    },
  },
};
```

The important part is `'load-extension=./',`, which tells Nightwatch to load our extension into the test browser.

And add the following to `wp.js` (replacing my login credentials with your own):

```javascript
module.exports = {
  'Test WordPress Mods': (browser) => {
    browser
      // Login to WP Dashboard
      .url('http://localhost/wp/wp-login.php')
      .setValue('#user_login', 'jim')
      .setValue('#user_pass', 'secret')
      .click('#wp-submit')

      // Go to New Post Page
      .url('http://localhost/wp/wp-admin/post-new.php')

      // Test MD > HTML conversion
      .setValue('#content', '## level 2 heading\n### level 3 heading')
      .click('input[value="MD"]')
      .assert.valueContains('#content', '<h2 id="level2heading">level 2 heading</h2>')
      .assert.valueContains('#content', '<h3 id="level3heading">level 3 heading</h3>')

      // This is here so that you can take a look at the browser
      .pause(5000)
      .end();
  },
};
```

Now run the tests using:

```bash
node_modules/.bin/nightwatch --config test/nightwatch.config.js
```

You should see an instance of the Chrome browser open and Nightwatch perform the tests we’ve specified. The result of the tests is output to the terminal.

Hopefully Nightwatch's DSL is pretty self explanatory. You can read more about it in their [documentation](https://nightwatchjs.org/api/). If you fancy a challenge, try adding tests for the datepicker.

Note that I've hardcoded my credentials here. If you use this for anything other than demonstration purposes, it'll be a good idea to move these to a config file that’s not committed to GitHub.

And don't forget you can find the code for everything I’ve demonstrated so far on [GitHub](https://github.com/sitepoint-editors/wp-enhance).

## Notable Features of SitePoint's Chrome Extension

As I'm sure you've realized, your mileage will vary regarding how useful you find such a browser extension. Most people will have (slightly) different needs and will be able to install WordPress plugins to solve most of the problems they encounter.

Nonetheless, in this final section, I'd like to outline some of the features we have added to our [SP-Tools extension](https://github.com/jameshibbard/SP-Tools) in the hope that they might inspire or even be useful for others.

*   A _Capitalize and Check_ button. This converts the post title to [title case](http://www.grammar-monster.com/lessons/capital_letters_title_case.htm).
*   A headline analysis tool, which gives you a score out of 100 for your title and offers suggestions for improvements.
*   A _Capitalize Subheadings_ button, which checks the remaining headings in the article for title capitalization.
*   A _Copy Link_ button, which copies the post's current permalink to the clipboard.
*   A _Rebuild Link_ button, which rebuilds the post's permalink. This is useful, for example, when WordPress creates a permalink based on a draft heading which subsequently changes.
*   An extensible [molly-guard](http://www.urbandictionary.com/define.php?term=molly-guard), which performs a number of checks and disables/enables the publish button accordingly. Among other things, it checks for:
    *   a sensible post permalink
    *   the presence of relative URLs in the editor pane
    *   the presence of empty links in the editor pane
    *   the presence of `<h1>` tags in the editor pane
    *   the presence of `[special]` shortcode tags in the excerpt
*   A _Copy Tags_ button, which gives you a comma-separated list of tags copied to the clipboard.
*   A _rel="sponsored"_ button, which toggles the `rel` attribute of all links in a post as being `sponsored`.

If you'd like to check it out, you can [find our extension on GitHub](https://github.com/jameshibbard/SP-Tools). There are a few other goodies in there, such as context menu entries, some code to turn off the infinite scroll on our main site and, of course, tests with Nightwatch.

## Conclusion

In this tutorial, we’ve looked at the various components that make up a Chrome extension. I’ve demonstrated how we can build and test our own Chrome extension to enhance the basic functionality of a WordPress install. I’ve also introduced you to SP-Tools, SitePoint's own Chrome extension, which we use to make various editing tasks somewhat easier.

If you find our extension useful, or adapt it to do anything else, I'd love to hear from you on [Twitter](https://twitter.com/jchibbard).