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

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, 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, 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 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 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 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, 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. The pipeline executes identically across browsers:

  1. Parse raw Markdown using the marked library
  2. Highlight code blocks via 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:

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:

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 creates the three distribution bundles:

// 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 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 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 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 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 script creates this variant by adjusting the archiver compression settings while including the exact same 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →