# Cross-Browser Extension Architecture for Chrome, Firefox, Opera, and Thunderbird

> Discover the unified WebExtension architecture powering Markdown Here across Chrome Firefox Opera and Thunderbird. Learn how a single codebase ensures consistent functionality with minimal browser-specific adjustments.

- Repository: [Adam Pritchard/markdown-here](https://github.com/adam-p/markdown-here)
- Tags: architecture
- Published: 2026-03-05

---

**Markdown Here uses a single WebExtension codebase that shares nearly all logic across Chrome, Firefox, Opera, and Thunderbird, differing only in the manifest-level browser ID and final packaging format.**

The `adam-p/markdown-here` repository demonstrates how modern browser extension development achieves true cross-platform compatibility through the WebExtension API. By structuring the project around a unified core with minimal platform-specific shims, the extension maintains identical functionality across Chromium-based browsers, Firefox, and Thunderbird while simplifying maintenance and feature parity.

## Unified WebExtension Architecture

### Centralized Manifest Configuration

The foundation of this cross-browser extension architecture resides in [`src/manifest.json`](https://github.com/adam-p/markdown-here/blob/main/src/manifest.json), which serves as the single source of truth for all supported browsers. The file declares **manifest_version: 3** and specifies `"service_worker": "chrome/backgroundscript.js"` in the background section, enabling Firefox and Thunderbird to run the same background code as Chrome and Opera through their shared WebExtension support.

The only browser-specific deviation occurs within the `browser_specific_settings.gecko.id` field, which provides the unique identifier required by Firefox and Thunderbird add-on directories. Every other permission, content script declaration, and options page reference remains identical across platforms.

### Shared Background Script Implementation

All browsers execute the logic defined in [`src/chrome/backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/backgroundscript.js), despite its Chrome-specific directory naming. In Firefox and Thunderbird, this script runs as a **service worker** due to the manifest_version 3 specification, while Chrome and Opera execute it as a standard background script. The WebExtension API abstracts these runtime differences, allowing the same event listeners to register the **"Markdown Toggle"** context menu entry and the `Alt+Shift+M` keyboard shortcut universally.

The background script listens for `chrome.runtime.onInstalled` to initialize the context menu, then handles click events through `chrome.contextMenus.onClicked`, injecting the content script via `chrome.scripting.executeScript` when users trigger the conversion.

### Universal Content Script Injection

The [`src/chrome/contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/contentscript.js) file handles DOM manipulation and Markdown conversion within compose windows across all four browsers. Because Thunderbird adopted the WebExtension model for its compose window and Firefox maintains full parity with Chrome's content script APIs, the same script detects editable text areas, inserts the toggle UI, and communicates with the background script without platform branches.

### Common Options Interface

User preferences are managed through [`src/common/options.html`](https://github.com/adam-p/markdown-here/blob/main/src/common/options.html) and its associated JavaScript, loaded via the `options_ui` manifest field. The page uses the `chrome.storage` API to persist settings, which automatically synchronizes across devices in Chrome and Firefox while maintaining local storage in Opera and Thunderbird.

## Build and Packaging Process

The [`utils/build.js`](https://github.com/adam-p/markdown-here/blob/main/utils/build.js) script generates distribution packages for all platforms in a single pass, handling the subtle compression requirements that differentiate the ecosystems. For Chrome, Opera, and Firefox, the script produces standard **ZIP** archives containing [`manifest.json`](https://github.com/adam-p/markdown-here/blob/main/manifest.json), the `common/` and `chrome/` directories, and `_locales/` folders.

Thunderbird requires an **XPI** package—a ZIP archive with a specific compression flag that the Firefox Add-ons site validates. The build script instantiates separate archiver instances for each target, applying the appropriate compression settings for the Thunderbird bundle while maintaining identical file contents across all three outputs.

## Runtime Execution Pipeline

When a user triggers the **"Markdown Toggle"** command, the background script executes the content script, which invokes the shared conversion library in [`src/common/markdown-here.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-here.js). The pipeline executes identically across browsers:

1. **Parse** raw Markdown using the `marked` library
2. **Highlight** code blocks via [`highlight.js`](https://github.com/adam-p/markdown-here/blob/main/highlight.js)
3. **Render** TeX expressions with `KaTeX` (if enabled in options)
4. **Sanitize** the resulting HTML using `DOMPurify`
5. **Replace** the textarea content while storing the original Markdown for toggle-back functionality

All storage operations use the standard `chrome.storage` API, which Firefox and Thunderbird implement as part of their WebExtension compatibility layer.

## Implementation Examples

The following code demonstrates the cross-browser background script registration:

```javascript
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "mdh-toggle",
    title: chrome.i18n.getMessage("contextMenuTitle"),
    contexts: ["editable"]
  });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "mdh-toggle") {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      files: ["chrome/contentscript.js"]
    });
  }
});

```

Content script execution imports from the common library and handles the conversion:

```javascript
import { markdownRender } from "../common/markdown-render.js";

function renderSelection() {
  const selection = window.getSelection().toString();
  if (!selection) return;

  const html = markdownRender(selection);
  document.execCommand("insertHTML", false, html);
}

```

The packaging script in [`utils/build.js`](https://github.com/adam-p/markdown-here/blob/main/utils/build.js) creates the three distribution bundles:

```javascript
// utils/build.js – excerpt
var chromeZip = new archiver('zip');
var firefoxZip = new archiver('zip');
var thunderbirdZip = new archiver('zip'); // xpi needs compression

// ...add files to each archive...
chromeZip.finalize();
firefoxZip.finalize();
thunderbirdZip.finalize();

```

## Summary

- **Single manifest**: [`src/manifest.json`](https://github.com/adam-p/markdown-here/blob/main/src/manifest.json) serves all browsers with only the `browser_specific_settings.gecko.id` varying between platforms
- **Universal scripts**: Background and content scripts in `src/chrome/` run unchanged across Chrome, Firefox, Opera, and Thunderbird thanks to WebExtension API standardization
- **Service worker compatibility**: Firefox and Thunderbird execute the background script as a service worker under manifest_version 3 without code modifications
- **Unified build**: The [`utils/build.js`](https://github.com/adam-p/markdown-here/blob/main/utils/build.js) script generates ZIP files for Chrome/Opera/Firefox and XPI files for Thunderbird from identical source trees
- **Shared conversion logic**: All Markdown parsing, syntax highlighting, and sanitization occurs in [`src/common/markdown-here.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-here.js) regardless of browser

## Frequently Asked Questions

### How does the same background script work in both Chrome and Firefox?

Firefox and Thunderbird implement the WebExtension API, including `chrome.runtime` and `chrome.contextMenus`, as part of their browser compatibility layer. When the manifest specifies `"service_worker": "chrome/backgroundscript.js"` under manifest_version 3, Firefox runs this file as a service worker while Chrome executes it as a background script, with both environments exposing identical global APIs.

### What is the only browser-specific difference in the manifest.json file?

The [`src/manifest.json`](https://github.com/adam-p/markdown-here/blob/main/src/manifest.json) file contains a single browser-specific field: `browser_specific_settings.gecko.id`, which provides the unique identifier required by Mozilla's add-on directory for Firefox and Thunderbird. All other permissions, script references, and metadata remain identical across Chrome, Opera, Firefox, and Thunderbird.

### Why does Thunderbird require an XPI file instead of a standard ZIP archive?

While the source code is identical, Thunderbird's add-on distribution mechanism requires **XPI** packaging—a ZIP archive with specific compression flags that the Mozilla Add-ons site validates during submission. The [`utils/build.js`](https://github.com/adam-p/markdown-here/blob/main/utils/build.js) script creates this variant by adjusting the archiver compression settings while including the exact same [`manifest.json`](https://github.com/adam-p/markdown-here/blob/main/manifest.json), `common/`, and `chrome/` directory contents as the standard Chrome and Firefox builds.

### Does the content script behavior differ between Chrome and Firefox when handling email compose windows?

No. The [`src/chrome/contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/contentscript.js) executes identical DOM manipulation and Markdown conversion logic across all supported browsers. Because Thunderbird adopted the WebExtension content script model for its compose windows and Firefox maintains full API parity with Chrome, the same code detects editable areas, renders Markdown through the shared library, and restores original text without platform-specific branches.