Every now and then, I want to build something that I would use. Coming from a web developer background, I’m under the impression that I have to build a web application.
One day I have this “a ha” moment:
Actually most of the time, I can just build a web extension instead of a web application for my needs!
Assuming that you are not creating a web extension that require backend, here’s some of the benefits building a web extension over a web application:
- No hosting fees. It will be running on the web browser.
- Your user data is owned by them. No need to deal with GDPR?
- You have access to a tons of amazing web browser APIs.
Through this post, you’ll get more understanding on the anatomy of a Firefox web extension. By the end of it, you’ll write a simple one.
Prerequisite
To build a web extension, all you need to know is the basic of:
- JavaScript
- HTML & CSS (optional, you don't need both for this post)
Here’s how this post will be structured:
- Basic structure of web extension
- Our first web extension - Be sure to read this before to jump into any of the below so that you have enough context.
- Setting up web extension
- Actual implementation
- Practice
- Closing
Basic structure of web extension
Web extension is HTML, CSS and JavaScript that run on the web browser with some restrictions and permissions.
Before writing any new kind of project, is always good to know the project structure and how it works at a high level.
Project structure
The most basic web extension project structure will looks something like this:
├── index.js
├── index.html (Optional)
├── icons
│ └── icon.png
└── manifest.json
According to Mozilla browser extension documentation,
manifest.json
is the only one you need:
This is the only file that must be present in every extension. It contains basic metadata such as its name, version, and the permissions it requires. It also provides pointers to other files in the extension.
The others common files you will have are:
index.js
, your JavaScript file that contains the implementation of your web extension.index.html
, if your web extension happens to use a pop-up/page/tab in the browser, this HTML file will represents the UI of the page.icons/icon.png
, to allow the web browser to display your extension icon.
High level architecture
Most of the time, your web extension will interact with the browser JavaScript APIs to achieve something you want.
Here’s the high level architecture of web extension:
User <-> Your Web Extension <-> Browser APIs
(this is not entirely accurate)
Our first web extension
As someone who uses Markdown for note taking and saves links I browsed day to day, I often find myself copy the title and the URL of a web page and note it down to my Markdown file.
After doing it again and again, I think I should build a
web extension to do that. This is the web extension we are
building today: ttmd
(short for title to markdown, what a creative name).
It converts the current active tab (web page) into Markdown link and copy it to user clipboard.
For example, by using our web extension now, it should copy the following to our clipboard:
[Writing a simple Firefox Extension | kw7oe](http://localhost:1313/posts/2022/01/31/writing-a-simple-firefox-extension/)
(is in localhost:1313 because this is written in my local environment)
Setting up web extension
Initialize project folder
Let’s create our web extension project folder:
mkdir my-ttmd
cd my-ttmd
touch manifest.json index.js
Writing manifest.json
In manifest.json
, add the following code:
{
"manifest_version": 2,
"name": "my-ttmd",
"description": "Title to Markdown",
"version": "0.0.1",
"browser_action": {
"default_icon": {},
"default_title": "Title to Markdown!"
},
"background": {
"scripts": ["index.js"]
},
"permissions": ["tabs"]
}
The first few keys are self explanatory. I’ll skip those and explain the following keys only:
browser_action
, to specify button attached to the Firefox toolbar.background
, to specify your backgroud scripts, which is where we write our code mainly.permissions
, to specify permissions we need to request from the user to use our browser extension. For more, you can read about it here.
Currently, we requested permissions for the tabs
API. We will be
requesting for more permissions as we needed.
Writing index.js
Since we state in the manifest that we will have a background script called
index.js
, let’s start writing some simple code:
In index.js
:
console.log("hello world")
Voila, our first extension is done. Classic hello world example. But how can we test it?
Testing browser extension
As stated in Mozilla tutorials, you can test your extension by temporarily loading your extension. Here’s the steps:
- Visit to
about:debugging
in Firefox. - Click
This Firefox
. - Click
Load Temporary Add-on
. - Choose your browser extension
manifest.json
.
After that, you should see your browser extension icon in the toolbar.
Now click our extension toolbar button and nothing will happen. Upon inspecting
our console, you’ll not see your hello world
as well.
So, how do we see our console log for our browser extension? Well, similar to above:
- Visit to
about:debugging
and clickThis Firefox
. - Click
Inspect
of your extension.
Now you should be able to see your hello world logged!
Given that we have gone through how to setup your first web extension project, write code and test the extension, we can now jump into the actual implementation.
Actual implementation
Let’s first describe in detail the behaviour of our browser extension before we jump into the implementation:
When we click the toolbar button, it should copy the current active tab title and URL as Markdown link into our clipboard.
Then, we can break it down further into four parts:
- Trigger an action when our toolbar button is clicked.
- Get the current tab title and URL.
- Format the title and URL into Markdown format.
- Copy the Markdown format result to the user clipboard.
Out of the four steps, all of them have some unknowns, except for the third step (which is just a simple string manipulation).
Given that, all we have to do is figure out:
- How to execute some code when our browser extension toolbar button is clicked in Firefox?
- How to get the browser current active tab information such as title and URL in Firefox?
- How to copy some string to user clipboard in browser in Firefox?
If you’re up to some challenge, you can stop reading this article and proceed to find out the answer yourself.
Else, here’s some the first few results I got from Google Search the questions above directly:
- Add a button to the toolbar - Mozilla | MDN
- Working with the Tabs API - Mozilla | MDN
- Interact with the clipboard - Mozilla | MDN
Go have a read and come back. By then, you should have enough information to write our first browser extension.
Execute code when toolbar button is clicked
From the first article, we learn that we can use browser.browserAction.onClicked.addListener(<function>)
to
execute code when our button is clicked.
So let’s start writing our actual implementation:
In index.js
:
- console.log("hello world")
+ const titleToMarkdown = () => {
+ console.log("hello world")
+ }
+ browser.browserAction.onClicked.addListener(titleToMarkdown);
Nothing hard and fancy here, we extract our console.log
into the
titleToMarkdown
function, and register our function with the browser action
listener using the provided API.
Now let’s test it out whether it works as expected. Visit to about:debugging
again and click This Firefox
, under your browser extension click the Reload
button.
Now you can click Inspect
button again and play around with your browser
extension
toolbar button. You should see hello world being logged multiple time.
Get the browser current active tab information
By reading through the documentation about working with the Tabs API, we know that
we could use browser.tabs.query({...})
to get information about user tabs in
browser.
However, since the documentation only mentioned about getting the current
window active tabs by listing all the current window tabs and looping through
each of it to find out which is active, let’s see if there’s a simpler way by
looking at the API references of tabs.query
.
Indeed there is, we can pass in active: true
to get the current active tabs
and currentWindow: true
to get the active tab of our current window.
The reason, we need to specify current window is because, a user might have multiple Firefox application open, but we are only interested in the current one they are looking at.
So, here’s how the code would look like in index.js
:
const titleToMarkdown = async () => {
- console.log("hello world");
+ let tabs = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ console.log({ tabs });
};
browser.browserAction.onClicked.addListener(titleToMarkdown);
Now, reload our browser extension and play around with it and we will be able to see the information we get.
Here’s what I get:
{
"tabs": [
{
"id": 326,
// other key value pairs ...
"url": "https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query",
"title": "tabs.query() - Mozilla | MDN"
}
]
}
Format it to Markdown
Well, for formatting is pretty straighforwad.
const titleToMarkdown = async () => {
let tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
+ let result = `[${tabs[0].title}](${tabs[0].url})`;
- console.log({ tabs });
+ console.log({ result });
};
browser.browserAction.onClicked.addListener(titleToMarkdown);
Copy to clipboard
Given the information we have from here, it should be straighforwad for us as well. Is all about calling the right API.
Let’s first add clipboardWrite
to our permissions
in manifest.json
as
mentioned in the documentation:
Using the API requires the permission “clipboardRead” or “clipboardWrite” in your manifest.json file
In manifest.json
:
- "permissions": ["tabs"]
+ "permissions": ["tabs", "clipboardWrite"]
Then all we have to do is called navigator.clipboard.writeText
in index.js
:
const titleToMarkdown = async () => {
let tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
let result = `[${tabs[0].title}](${tabs[0].url})`;
+ navigator.clipboard.writeText(result).then(
+ function () {
+ console.log("successfully copied to clipboard!");
+ },
+ function () {
+ console.log("failed copied to clipboard!");
+ }
+ );
- console.log({ result });
};
browser.browserAction.onClicked.addListener(titleToMarkdown);
Be sure to reload your browser extension again every time you make the changes. Now by clicking our toolbar button, we should able to successfully copy the current active tab as Markdown link.
🎉 In just 19 lines of code, we write our first web extension.
Practice
But something is lacking right? It’s not obvious for our user to know if the Markdown link is successfully copied.
Now it’s your turn to implement that!
Purposely left blank for those who want to implement the notification part without any spoilers or hint
…
…
…
…
…
My solution
Similar with the approach I took above, this is what I would Google search:
Your result may vary but the second result of it is what I’m looking for:
Given this information, if you haven’t implement it your own as a practice, this is your last resort before I show my final code.
Last resort to implement it for anyone who’s interested to
…
…
…
…
…
So, here’s how my final code looks like:
In manifest.json
:
- "permissions": ["tabs", "clipboardWrite"]
+ "permissions": ["tabs", "notifications", "clipboardWrite"]
In index.js
:
const titleToMarkdown = async () => {
let tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
let result = `[${tabs[0].title}](${tabs[0].url})`;
navigator.clipboard.writeText(result).then(
+ async function () {
+ await browser.notifications.create({
+ type: "basic",
+ title: "🎉 Sucess!",
+ message:
+ " Successfully copy the title and URL for this page to your clipboard!",
+ });
console.log("successfully copied to clipboard!");
},
function () {
console.log("failed copied to clipboard!");
}
);
};
browser.browserAction.onClicked.addListener(titleToMarkdown);
That’s all we have for today. If you implemented yourself, you might have a different implementation and that’s okay. There’s no right and wrong as long as we achieve our goal of notifiying the user.
Closing
If you need the full code example, here’s the repository to my implementation. It might have slight differences but majority of it should be the same.
Hopefully, this open up the path to you all to the world of developing browser extension. It is always good to add another tool to your tool kit.
Anyway, the key takeaways shouldn’t be just the code we have written, but the approach we took to figure out the unknowns.