DEV Community

Simon Cleriot
Simon Cleriot

Posted on

Build a Firefox extension step-by-step

Lately I've been using the reading list on dev.to. It's a nice tool, but I'm used to saving articles for later in Pocket.
In this article, we're going to create a Firefox extension to automatically add a post to your Dev.to reading list and your Pocket account at the same time.

Here is what it looks like (the extension file is available at the end of this article):

The extension expects you to be already connected to a Pocket account in your browser (so we don't have to handle API authentification).

What is a browser extension?

A browser extension is a collection of scripts executed when Firefox browses to specific pages. Those scripts can alter the HTML, CSS and JavaScript of a page, and have access to specific JavaScript APIs (bookmarks, identity, etc...).

There are two types of scripts: content and background. Content scripts are executed within the page whereas background scripts perform long-term operations and maintain long-term state. Background scripts also have access to all the WebExtension API.

Here is the final file structure for this project:

  • manifest.json (configuration file)
  • background.js (our background script)
  • devtopocket.js (content script executed on the dev.to page)
  • images/

Content and background scripts

We have two scripts in our project: one that handles background work (sending the Ajax request) and another one (a content script) that registers a click event on the "Reading list" Dev.to button:

Content script

The content script (devtopocket.js) registers the click and sends the request to our background script.

devtopocket.js

document.getElementById("reaction-butt-readinglist").addEventListener("click", function() {
    if(window.confirm("Do you want to save this article in Pocket?")) {
        sendBackgroundToPocket();
    }
});
Enter fullscreen mode Exit fullscreen mode

The sendBackgroundToPocket method needs to communicate with the background script and ask it to send the Ajax request.

browser.runtime gives us a two way communication channel between our extension scripts. browser.runtime.sendMessage sends a message on that channel and returns a Promise that waits for a response on the other side. Once we get the answer (meaning the Ajax request has completed), a message is displayed to the user (cf the above gif):

devtopocket.js

function sendBackgroundToPocket(){
    browser.runtime.sendMessage({"url": window.location.href}).then(function(){
        document.getElementById("article-reaction-actions").insertAdjacentHTML("afterend", "<div id='devtopocket_notification' style='text-align: center;padding: 10px 0px 28px;'>This article has been saved to Pocket!</div>")
        setTimeout(function(){
            document.getElementById("devtopocket_notification").remove()
        }, 2000)
    });  
}
Enter fullscreen mode Exit fullscreen mode

Background script

A background script is used to write time consuming operations that do not depend on a specific web page being opened.

These scripts are loaded with the extension, and are executed until the extension is disabled or uninstalled.

Our background script (background.js) has two roles:

  • Sending the Ajax request
  • Reacting to URL changes via History API

In the extension configuration (manifest.json below), we're going to say "load devtopocket.js on pages matching an url pattern" and it works when we browse straight to an article page.

The "issue" with dev.to website is that it uses HTML5 History api to browse pages (as does every single page webapp). Firefox doesn't listen for url changes if the page isn't fully reloaded and therefore doesn't execute our content script. That's why we're going to need a background script to listen for url changes via History API, and manually execute the frontend script when needed.

We listen to url changes by using the webNavigation API:

background.js

browser.webNavigation.onHistoryStateUpdated.addListener(function(details) {
    browser.tabs.executeScript(null,{file:"devtopocket.js"});
}, {
    url: [{originAndPathMatches: "^.+://dev.to/.+/.+$"}]
});
Enter fullscreen mode Exit fullscreen mode

{originAndPathMatches: "^.+://dev.to/.+/.+$"} restricts the listener to a specific target url pattern (the same as the one we're also going to define in our manifest.json).

The browser.tabs.executeScript method loads a content script in the current tab.

The background scripts expects a message from our content script (when the "Reading list" button is clicked):

background.js

function handleMessage(message, sender, sendResponse) {
    if(message.url) {
        sendToPocket(message.url, sendResponse)
        return true;
    }
}
browser.runtime.onMessage.addListener(handleMessage)
Enter fullscreen mode Exit fullscreen mode

The sendToPocket method is called upon message receiving.
To save our url in Pocket, we're going to call the existing save page provided by Pocket (https://getpocket.com/save). A classic Ajax request will do the trick:

function sendToPocket(url, sendResponse) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
            sendResponse();
        }
    };
    xhr.open("GET", "https://getpocket.com/save?url="+url, true);
    xhr.send();
}
Enter fullscreen mode Exit fullscreen mode

You might see coming the Cross Origin Request problem, we'll address it later with the extension permissions.

The manifest

manifest.json is our extension configuration file. It's like a package.json in a javascript webapp or an AndroidManifest.xml in an Android app. You define the version and name of your project, permissions that you need and JavaScript source files that compose your extension.

First we write the app definition:

{
    "manifest_version": 2,
    "name": "DevToPocket",
    "version": "1.0.0",

    "description": "Send your DEV.to reading list to Pocket",

    "icons": {
        "48": "icons/devtopocket-48.png"
    },
    ...
}
Enter fullscreen mode Exit fullscreen mode

Supply at least a 48x48 icon, if you supply more sizes Firefox will try to use the best icon size depending on your screen resolution. We're going to use this icon:

icons/devtopocket-48.png

Then we define our permissions:

{
    ...
    "permissions": [
        "storage",
        "cookies",
        "webNavigation",
        "tabs",
        "*://dev.to/*/*",
        "*://getpocket.com/*"
    ]
}
Enter fullscreen mode Exit fullscreen mode

You can find the permissions list in the Mozilla documentation.

URLs in the permissions gives our extension extended privileges. In our case, it gives us access to getpocket.com from dev.to without cross-origin restrictions, we can inject a script in dev.to via tabs.executeScript and we have access to getpocket.com cookies so the Ajax request is authentificated. The full host permissions list is available here.

The full manifest.json file:

{
    "manifest_version": 2,
    "name": "DevToPocket",
    "version": "1.0.0",

    "description": "Send your DEV.to reading list to Pocket",

    "icons": {
        "48": "icons/devtopocket-48.png"
    },

    "content_scripts": [
        {
            "matches": ["*://dev.to/*/*"],
            "js": ["devtopocket.js"]
        }
    ],
    "background": {
        "scripts": ["background.js"]
    },

    "permissions": [
        "storage",
        "cookies",
        "webNavigation",
        "tabs",
        "*://dev.to/*/*",
        "*://getpocket.com/*"
    ]
}
Enter fullscreen mode Exit fullscreen mode

Run the extension

In order to run your extension, we are going to use the web-ext command line: https://github.com/mozilla/web-ext

This is a command line tool to help build, run, and test WebExtensions.

npm install --global web-ext
Enter fullscreen mode Exit fullscreen mode

Then in your terminal, run the following command in your project folder:

web-ext run
Enter fullscreen mode Exit fullscreen mode

It's going to launch a browser with your extension temporarily loaded. The extension is automatically reloaded when you make some changes.

Sign the extension

To install your extension in someone else's browser, you'll need to package and sign the extension.

First create a developer account on the Mozilla Developer Hub then retrieve your API credentials here: https://addons.mozilla.org/en-US/developers/addon/api/key/

Run the web-ext sign command:

web-ext sign --api-key=user:XXX --api-secret=YYY
Enter fullscreen mode Exit fullscreen mode

Your extension file will be available afterwards in web-ext-artifacts/devtopocket-X.X.X-an+fx.xpi. Open the file in Firefox to install it.


The complete source code is available on GitHub: https://github.com/scleriot/devtopocket
You can download and install the latest release: https://github.com/scleriot/devtopocket/releases/latest

This extension also works with Firefox for Android!

Top comments (3)

Collapse
 
fengshangwuqi profile image
枫上雾棋

Nice case, I wrote a post 程序员偷懒指南 -- 使用 chrome 扩展实现前端资讯推送 last month, but I also get something else from this post, Thanks.

Collapse
 
andy profile image
Andy Zhao (he/him)

So cool! Nice job dude! We recently had someone say they wanted a feature that allowed them to save dev.to articles via Pocket. Can I share your project on that issue?

Collapse
 
scleriot profile image
Simon Cleriot

Yes of course :)

The extension is available here: github.com/scleriot/devtopocket/re...
I should publish it on Firefox addons platform so it's more convenient to install