Sponsored PostCreating One Browser Extension For All Browsers: Edge, Chrome, Firefox, Opera, Brave And Vivaldi
- By David Rousset
- April 5th, 2017
- BrowsersCSSExtensions
- 1 Comment
In today’s article, we’ll create a JavaScript extension that works in all major modern browsers, using the very same code base. Indeed, the Chrome extension model based on HTML, CSS and JavaScript is now available almost everywhere, and there is even a Browser Extension Community Group1 working on a standard.
I’ll explain how you can install this extension that supports the web extension model (i.e. Edge, Chrome, Firefox, Opera, Brave and Vivaldi), and provide some simple tips on how to get a unique code base for all of them, but also how to debug in each browser.
Note: We won’t cover Safari in this article because it doesn’t support the same extension model2 as others.
Further Reading on SmashingMag: Link
- Creating A “Save For Later” Chrome Extension With Modern Web Tools3
- What’s The Deal With The Samsung Internet Browser?4
- Form Inputs: The Browser Support Issue You Didn’t Know You Had5
- Chrome, Firefox, Safari, Opera, Edge? Impressive Web Browser Alternatives6
Basics Link
I won’t cover the basics of extension development because plenty of good resources are already available from each vendor:
- Google7
- Microsoft8 (also, see the great overview video “Building Extensions for Microsoft Edge9”)
- Mozilla10 (also, see the wiki11)
- Opera12
- Brave13
So, if you’ve never built an extension before or don’t know how it works, have a quick look at those resources. Don’t worry: Building one is simple and straightforward.
Our Extension Link
Let’s build a proof of concept — an extension that uses artificial intelligence (AI) and computer vision to help the blind analyze images on a web page.
We’ll see that, with a few lines of code, we can create some powerful features in the browser. In my case, I’m concerned with accessibility on the web and I’ve already spent some time thinking about how to make a breakout game accessible using web audio and SVG14, for instance.
Still, I’ve been looking for something that would help blind people in a more general way. I was recently inspired while listening to a great talk by Chris Heilmann15 in Lisbon: “Pixels and Hidden Meaning in Pixels16.”
Indeed, using today’s AI algorithms in the cloud, as well as text-to-speech technologies, exposed in the browser with the Web Speech API17 or using a remote cloud service, we can very easily build an extension that analyzes web page images with missing or improperly filled alt
text properties.
My little proof of concept simply extracts images from a web page (the one in the active tab) and displays the thumbnails in a list. When you click on one of the images, the extension queries the Computer Vision API to get some descriptive text for the image and then uses either the Web Speech API or Bing Speech API to share it with the visitor.
The video below demonstrates it in Edge, Chrome, Firefox, Opera and Brave.
You’ll notice that, even when the Computer Vision API is analyzing some CGI images, it’s very accurate! I’m really impressed by the progress the industry has made on this in recent months.
I’m using these services:
- Computer Vision API18, Microsoft Cognitive Services
This is free to use19 (with a quota). You’ll need to generate a free key; replace theTODO
section in the code with your key to make this extension work on your machine. To get an idea of what this API can do, play around with it20. - Bing Text to Speech API22, Microsoft Cognitive Services
This is also free to use23 (with a quota, too). You’ll need to generate a free key again. We’ll also use a small library24 that I wrote recently to call this API from JavaScript. If you don’t have a Bing key, the extension will always fall back to the Web Speech API, which is supported by all recent browsers.
But feel free to try other similar services:
- Visual Recognition25, IBM Watson
- Cloud Vision API26, Google
You can find the code for this small browser extension on my GitHub page27. Feel free to modify the code for other products you want to test.
Tip To Make Your Code Compatible With All Browsers Link
Most of the code and tutorials you’ll find use the namespace chrome.xxx
for the Extension API (chrome.tabs
, for instance).
But, as I’ve said, the Extension API model is currently being standardized to browser.xxx
, and some browsers are defining their own namespaces in the meantime (for example, Edge is using msBrowser
).
Fortunately, most of the API remains the same behind the browser. So, it’s very simple to create a little trick to support all browsers and namespace definitions, thanks to the beauty of JavaScript:
window.browser = (function () { return window.msBrowser || window.browser || window.chrome; })();
And voilà!
Of course, you’ll also need to use the subset of the API supported by all browsers. For instance:
- Microsoft Edge has a list of support28.
- Mozilla Firefox shares its current Chrome incompatibilities29.
- Opera maintains its own list of extension APIs supported30 by its browser.
Extension Architecture Link
Let’s review together the architecture of this extension. If you’re new to browser extensions, this should help you to understand the flow.
Let’s start with the manifest file31:
This manifest file and its associated JSON is the minimum you’ll need to load an extension in all browsers, if we’re not considering the code of the extension itself, of course. Please check the source34 in my GitHub account, and start from here to be sure that your extension is compatible with all browsers.
For instance, you must specify an author
property to load it in Edge; otherwise, it will throw an error. You’ll also need to use the same structure for the icons. The default_title
property is also important because it’s used by screen readers in some browsers.
Here are links to the documentation to help you build a manifest file that is compatible everywhere:
The sample extension used in this article is mainly based on the concept of the content script38. This is a script living in the context of the page that we’d like to inspect. Because it has access to the DOM, it will help us to retrieve the images contained in the web page. If you’d like to know more about what a content script is, Opera39, Mozilla40 and Google41 have documentation on it.
Our content script42 is simple:
console.log("Dare Angel content script started"); browser.runtime.onMessage.addListener(function (request, sender, sendResponse) { if (request.command == "requestImages") { var images = document.getElementsByTagName('img'); var imagesList = []; for (var i = 0; i 64 && images[i].height > 64)) { imagesList.push({ url: images[i].src, alt: images[i].alt }); } } sendResponse(JSON.stringify(imagesList)); } }); view raw
This first logs into the console to let you check that the extension has properly loaded. Check it via your browser’s developer tool, accessible from F12
, Control + Shift + I
or ⌘ + ⌥ + I
.
It then waits for a message from the UI page with a requestImages
command to get all of the images available in the current DOM, and then it returns a list of their URLs if they’re bigger than 64 × 64 pixels (to avoid all of the pixel-tracking junk and low-resolution images).
The popup UI page47 we’re using is very simple and will display the list of images returned by the content script inside a flexbox container48. It loads the start.js
script, which immediately creates an instance of dareangel.dashboard.js
49 to send a message to the content script to get the URLs of the images in the currently visible tab.
Here’s the code that lives in the UI page, requesting the URLs to the content script:
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => { browser.tabs.sendMessage(tabs[0].id, { command: "requestImages" }, (response) => { this._imagesList = JSON.parse(response); this._imagesList.forEach((element) => { var newImageHTMLElement = document.createElement("img"); newImageHTMLElement.src = element.url; newImageHTMLElement.alt = element.alt; newImageHTMLElement.tabIndex = this._tabIndex; this._tabIndex++; newImageHTMLElement.addEventListener("focus", (event) => { if (COMPUTERVISIONKEY !== "") { this.analyzeThisImage(event.target.src); } else { var warningMsg = document.createElement("div"); warningMsg.innerHTML = "
Please generate a Computer Vision key in the other tab. Link
"; this._targetDiv.insertBefore(warningMsg, this._targetDiv.firstChild); browser.tabs.create({ active: false, url: "https://www.microsoft.com/cognitive-services/en-US/sign-up?ReturnUrl=/cognitive-services/en-us/subscriptions?productId=%2fproducts%2f54d873dd5eefd00dc474a0f4" }); } }); this._targetDiv.appendChild(newImageHTMLElement); }); }); });
We’re creating image elements. Each image will trigger an event if it has focus, querying the Computer Vision API for review.
This is done by this simple XHR call:
analyzeThisImage(url) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState == 4 && xhr.status == 200) { var response = document.querySelector('#response'); var reponse = JSON.parse(xhr.response); var resultToSpeak = `With a confidence of ${Math.round(reponse.description.captions[0].confidence * 100)}%, I think it's ${reponse.description.captions[0].text}`; console.log(resultToSpeak); if (!this._useBingTTS || BINGSPEECHKEY === "") { var synUtterance = new SpeechSynthesisUtterance(); synUtterance.text = resultToSpeak; window.speechSynthesis.speak(synUtterance); } else { this._bingTTSclient.synthesize(resultToSpeak); } } }; xhr.onerror = (evt) => { console.log(evt); }; try { xhr.open('POST', 'https://api.projectoxford.ai/vision/v1.0/describe'); xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Ocp-Apim-Subscription-Key", COMPUTERVISIONKEY); var requestObject = { "url": url }; xhr.send(JSON.stringify(requestObject)); } catch (ex) { console.log(ex); } } view raw
The following articles will you help you to understand how this Computer Vision API works:
- “Analyzing an Image Version 1.050,” Microsoft Cognitive Services
- “Computer Vision API, v1.051,” Microsoft Cognitive Services
This shows you via an interactive console in a web page how to call the REST API with the proper JSON properties, and the JSON object you’ll get in return. It’s useful to understand how it works and how you will call it.
In our case, we’re using the describe
feature of the API. You’ll also notice in the callback that we will try to use either the Web Speech API or the Bing Text-to-Speech service, based on your options.
Here, then, is the global workflow of this little extension:
Loading The Extension In Each Browser Link
Let’s review quickly how to install the extension in each browser.
Prerequisites Link
Download or clone my small extension54 from GitHub somewhere to your hard drive.
Also, modify dareangel.dashboard.js
to add at least a Computer Vision API key. Otherwise, the extension will only be able to display the images extracted from the web page.
Microsoft Edge Link
First, you’ll need at least a Windows 10 Anniversary Update (OS Build 14393+) to have support for extensions in Edge.
Then, open Edge and type about:flags
in the address bar. Check the “Enable extension developer features.”
Click on “…” in the Edge’s navigation bar and then “Extensions” and then “Load extension,” and select the folder where you’ve cloned my GitHub repository. You’ll get this:
Click on this freshly loaded extension, and enable “Show button next to the address bar.”
Note the “Reload extension” button, which is useful while you’re developing your extension. You won’t be forced to remove or reinstall it during the development process; just click the button to refresh the extension.
Navigate to BabylonJS626158, and click on the Dare Angel (DA) button to follow the same demo as shown in the video.
Google Chrome, Opera, Vivaldi Link
In Chrome, navigate to chrome://extensions
. In Opera, navigate to opera://extensions
. And in Vivaldi, navigate to vivaldi://extensions
. Then, enable “Developer mode.”
Click on “Load unpacked extension,” and choose the folder where you’ve extracted my extension.
Navigate to BabylonJS626158, and open the extension to check that it works fine.
Mozilla Firefox Link
You’ve got two options here. The first is to temporarily load your extension, which is as easy as it is in Edge and Chrome.
Open Firefox, navigate to about:debugging
and click “Load Temporary Add-on.” Then, navigate to the folder of the extension, and select the manifest.json
file. That’s it! Now go to BabylonJS626158 to test the extension.
The only problem with this solution is that every time you close the browser, you’ll have to reload the extension. The second option would be to use the XPI packaging. You can learn more about this in “Extension Packaging65” on the Mozilla Developer Network.
Brave Link
The public version of Brave doesn’t have a “developer mode” embedded in it to let you load an unsigned extension. You’ll need to build your own version of it by following the steps in “Loading Chrome Extensions in Brave66.”
As explained in that article, once you’ve cloned Brave, you’ll need to open the extensions.js
file in a text editor. Locate the lines below, and insert the registration code for your extension. In my case, I’ve just added the two last lines:
// Manually install the braveExtension and torrentExtension extensionInfo.setState(config.braveExtensionId, extensionStates.REGISTERED) loadExtension(config.braveExtensionId, getExtensionsPath('brave'), generateBraveManifest(), 'component') extensionInfo.setState('DareAngel', extensionStates.REGISTERED) loadExtension('DareAngel', getExtensionsPath('DareAngel/')) view raw
Copy the extension to the app/extensions
folder. Open two command prompts in the browser-laptop
folder. In the first one, launch npm run watch
, and wait for webpack to finish building Brave’s Electron app. It should say, “webpack: bundle is now VALID.” Otherwise, you’ll run into some issues.
Then, in the second command prompt, launch npm start
, which will launch our slightly custom version of Brave.
In Brave, navigate to about:extensions
, and you should see the extension displayed and loaded in the address bar.
Debugging The Extension In Each Browser Link
Tip for all browsers: Using console.log()
, simply log some data from the flow of your extension. Most of the time, using the browser’s developer tools, you’ll be able to click on the JavaScript file that has logged it to open it and debug it.
Microsoft Edge Link
To debug the client script part, living in the context of the page, you just need to open F12
. Then, click on the “Debugger” tab and find your extension’s folder.
Open the script file that you’d like to debug — dareangel.client.js
, in my case — and debug your code as usual, setting up breakpoints, etc.
If your extension creates a separate tab to do its job (like the Page Analyzer73, which our Vorlon.js74 team published in the store), simply press F12
on that tab to debug it.
If you’d like to debug the popup page, you’ll first need to get the ID of your extension. To do that, simply go into the property of the extension and you’ll find an ID property:
Then, you’ll need to type in the address bar something like ms-browser-extension://ID_of_your_extension/yourpage.html
. In our case, it would be ms-browser-extension://DareAngel_vdbyzyarbfgh8/dashboard.html
. Then, simply use F12
on this page:
Google Chrome, Opera, Vivaldi, Brave Link
Because Chrome and Opera rely on the same Blink code base, they share the same debugging process. Even though Brave and Vivaldi are forks of Chromium, they also share the same debugging process most of the time.
To debug the client script part, open the browser’s developer tools on the page that you’d like to debug (pressing F12
, Control + Shift + I
or ⌘ + ⌥ + I
, depending on the browser or platform you’re using).
Then, click on the “Content scripts” tab and find your extension’s folder. Open the script file that you’d like to debug, and debug your code just as you would do with any JavaScript code.
To debug a tab that your extension would create, it’s exactly the same as with Edge: Simply use the developer tools.
For Chrome and Opera, to debug the popup page, right-click on the button of your extension next to the address bar and choose “Inspect popup,” or open the HTML pane of the popup and right-click inside it to “Inspect.” Vivaldi only supports right-click and then “Inspect” inside the HTML pane once opened.
For Brave, it’s the same process as with Edge. You first need to find the GUID associated with your extension in about:extensions
:
And then, in a separate tab, open the page you’d like to debug like — in my case, chrome-extension://bodaahkboijjjodkbmmddgjldpifcjap/dashboard.html
— and open developer tools.
For the layout, you have a bit of help using Shift + F8
, which will let you inspect the complete frame of Brave. And you’ll discover that Brave is an Electron app using React!
Note, for instance, the data-reactroot
attribute.
Note: I had to slightly modify the CSS of the extension for Brave because it currently displays popups with a transparent background by default, and I also had some issues with the height of my images collection. I’ve limited it to four elements in Brave.
Mozilla Firefox Link
Mozilla has really great documentation on debugging web extensions91.
For the client script part, it’s the same as in Edge, Chrome, Opera and Brave. Simply open the developer tools in the tab you’d like to debug, and you’ll find a moz-extension://guid
section with your code to debug:
If you need to debug a tab that your extension would create (like Vorlon.js’ Page Analyzer extension), simply use the developer tools:
Finally, debugging a popup is a bit more complex but is well explained in the “Debugging Popups96” section of the documentation.
Publishing Your Extension In Each Store Link
Each vendor has detailed documentation on the process to follow to publish your extension in its store. They all take similar approaches. You need to package the extension in a particular file format — most of the time, a ZIP-like container. Then, you have to submit it in a dedicated portal, choose a pricing model and wait for the review process to complete. If accepted, your extension will be downloadable in the browser itself by any user who visits the extensions store.
Here are the various processes:
- Google: “Publish in the Chrome Web Store99”
- Mozilla: “Publishing your WebExtension100”
- Opera: “Publishing Guidelines101”
- Microsoft: “Packaging Microsoft Edge Extensions102”
Please note that submitting a Microsoft Edge extension to the Windows Store is currently a restricted capability. Reach out to the Microsoft Edge team103 with your request to be a part of the Windows Store, and they’ll consider you for a future update.
I’ve tried to share as much of what I’ve learned from working on our Vorlon.js Page Analyzer extension104 and this little proof of concept.
Some developers remember the pain of working through various implementations to build their extension — whether it meant using different build directories, or working with slightly different extension APIs, or following totally different approaches, such as Firefox’s XUL extensions or Internet Explorer’s BHOs and ActiveX.
It’s awesome to see that, today, using our regular JavaScript, CSS and HTML skills, we can build great extensions using the very same code base and across all browsers!
Feel free to ping me on Twitter105 for any feedback.
(ms, vf, rb, yk, al, il)
Footnotes Link
- 1 https://browserext.github.io/
- 2 https://developer.apple.com/reference/safariextensions
- 3 https://www.smashingmagazine.com/2014/11/creating-save-later-chrome-extension-modern-web-tools/
- 4 https://www.smashingmagazine.com/2016/10/whats-the-deal-with-the-samsung-internet-browser/
- 5 https://www.smashingmagazine.com/2015/05/form-inputs-browser-support-issue/
- 6 https://www.smashingmagazine.com/2015/09/chrome-firefox-safari-opera-edge-impressive-web-browser-alternatives/
- 7 https://developer.chrome.com/extensions
- 8 https://developer.microsoft.com/en-us/microsoft-edge/platform/documentation/extensions/
- 9 https://channel9.msdn.com/Events/WebPlatformSummit/edgesummit2016/ES1614?wt.mc_id=DX_879946&OC.ID=DX_879946&CR_CC=DX_879946
- 10 https://developer.mozilla.org/en-US/Add-ons/WebExtensions
- 11 https://wiki.mozilla.org/WebExtensions
- 12 https://dev.opera.com/extensions/getting-started/
- 13 https://github.com/brave/browser-laptop/wiki/Developer-Notes-on-Installing-or-Updating-Extensions
- 14 https://www.davrous.com/2015/08/27/creating-an-accessible-breakout-game-using-web-audio-svg/
- 15 https://twitter.com/codepo8
- 16 https://www.youtube.com/watch?v=TWVNTsdm27U
- 17 https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html#tts-section
- 18 https://www.microsoft.com/cognitive-services/en-us/computer-vision-api
- 19 https://www.microsoft.com/cognitive-services/en-us/subscriptions?productId=/products/54d873dd5eefd00dc474a0f4
- 20 https://www.captionbot.ai/
- 21 https://www.smashingmagazine.com/wp-content/uploads/2016/12/CaptionBot-1-preview-opt.png
- 22 https://www.microsoft.com/cognitive-services/en-us/speech-api
- 23 https://www.microsoft.com/cognitive-services/en-us/subscriptions?productId=/products/Bing.Speech.Preview
- 24 https://github.com/davrous/BingTTSClientJSLib
- 25 https://visual-recognition-demo.mybluemix.net/
- 26 https://cloud.google.com/vision/
- 27 http://github.com/davrous/dareangel
- 28 https://developer.microsoft.com/en-us/microsoft-edge/platform/documentation/extensions/api-support/
- 29 https://developer.mozilla.org/en-US/Add-ons/WebExtensions
- 30 https://dev.opera.com/extensions/apis/
- 31 https://github.com/davrous/dareangel/blob/master/manifest.json
- 32 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide1-large-opt.png
- 33 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide1-large-opt.png
- 34 https://github.com/davrous/dareangel/blob/master/manifest.json
- 35 https://developer.chrome.com/extensions/manifest
- 36 https://developer.microsoft.com/en-us/microsoft-edge/platform/documentation/extensions/api-support/supported-manifest-keys/
- 37 https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json
- 38 https://dev.opera.com/extensions/architecture-overview/#the-content-script
- 39 https://dev.opera.com/extensions/content-scripts/
- 40 https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts
- 41 https://developer.chrome.com/extensions/content_scripts
- 42 https://github.com/davrous/dareangel/blob/master/dareangel.client.ts
- 43 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide2-large-opt.png
- 44 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide2-large-opt.png
- 45 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide3-large-opt.png
- 46 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Slide3-large-opt.png
- 47 https://github.com/davrous/dareangel/blob/master/dashboard.html
- 48 https://github.com/davrous/dareangel/blob/master/dashboard.css
- 49 https://github.com/davrous/dareangel/blob/master/dareangel.dashboard.js
- 50 https://www.microsoft.com/cognitive-services/en-us/Computer-Vision-API/documentation/AnalyzeImage
- 51 https://dev.projectoxford.ai/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa
- 52 https://www.smashingmagazine.com/wp-content/uploads/2016/12/xSlide4_thumb-large-opt.png
- 53 https://www.smashingmagazine.com/wp-content/uploads/2016/12/xSlide4_thumb-large-opt.png
- 54 https://github.com/davrous/dareangel
- 55 https://www.smashingmagazine.com/wp-content/uploads/2016/12/EnableInEdge001-1-preview-opt.png
- 56 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Edge001-preview-opt.png
- 57 https://www.smashingmagazine.com/wp-content/uploads/2016/12/EnableInEdge003-1-preview-opt.png
- 58 http://www.babylonjs.com/
- 59 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Chrome001-large-opt.png
- 60 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Chrome001-large-opt.png
- 61 http://www.babylonjs.com/
- 62 http://www.babylonjs.com/
- 63 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Firefox001-large-opt.png
- 64 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Firefox001-large-opt.png
- 65 https://developer.mozilla.org/en-US/Add-ons/Extension_Packaging
- 66 https://blog.brave.com/loading-chrome-extensions-in-brave/
- 67 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Brave001-large-opt.png
- 68 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Brave001-large-opt.png
- 69 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Brave002-large-opt.png
- 70 https://www.smashingmagazine.com/wp-content/uploads/2016/12/Brave002-large-opt.png
- 71 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge001-large-opt.png
- 72 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge001-large-opt.png
- 73 https://www.microsoft.com/store/p/page-analyzer/9nblggh4qws7
- 74 http://www.vorlonjs.io/
- 75 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge002-large-opt.png
- 76 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge002-large-opt.png
- 77 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge003-preview-opt.png
- 78 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge004-large-opt.png
- 79 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInEdge004-large-opt.png
- 80 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome001-large-opt.png
- 81 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome001-large-opt.png
- 82 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome002-large-opt.png
- 83 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome002-large-opt.png
- 84 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome003-1-large-opt.jpg
- 85 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInChrome003-1-large-opt.jpg
- 86 https://www.smashingmagazine.com/wp-content/uploads/2016/12/BraveDebug001-preview-opt.png
- 87 https://www.smashingmagazine.com/wp-content/uploads/2016/12/BraveDebug002-large-opt.png
- 88 https://www.smashingmagazine.com/wp-content/uploads/2016/12/BraveDebug002-large-opt.png
- 89 https://www.smashingmagazine.com/wp-content/uploads/2016/12/BraveDebug003-large-opt.jpg
- 90 https://www.smashingmagazine.com/wp-content/uploads/2016/12/BraveDebug003-large-opt.jpg
- 91 https://developer.mozilla.org/fr/Add-ons/WebExtensions/Debugging
- 92 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox001-large-opt.png
- 93 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox001-large-opt.png
- 94 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox002-large-opt.png
- 95 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox002-large-opt.png
- 96 https://developer.mozilla.org/fr/Add-ons/WebExtensions/Debugging
- 97 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox003-large-opt.jpg
- 98 https://www.smashingmagazine.com/wp-content/uploads/2016/12/DebugInFirefox003-large-opt.jpg
- 99 https://developer.chrome.com/webstore/publish
- 100 https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Publishing_your_WebExtension
- 101 https://dev.opera.com/extensions/publishing-guidelines/
- 102 https://docs.microsoft.com/en-us/microsoft-edge/extensions/guides/packaging
- 103 http://aka.ms/extension-request
- 104 https://www.microsoft.com/store/p/page-analyzer/9nblggh4qws7
- 105 https://twitter.com/davrous