# Architectural Difference Between Content Scripts and Background Scripts in Markdown Here

> Explore the architectural difference between content scripts and background scripts in the Markdown Here extension. Understand their distinct roles in rendering and management.

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

---

**Content scripts run inside the web page context to detect editable elements and delegate rendering, while background scripts operate in the privileged extension environment to manage UI, store settings, and perform the actual Markdown-to-HTML conversion.**

The adam-p/markdown-here extension implements a strict separation between content scripts and background scripts to balance security with functionality. Understanding this architectural pattern is essential for developers building browser extensions that must interact with web page content while maintaining access to sensitive browser APIs.

## Execution Environment and Privilege Boundaries

Content scripts execute within the target page's context as defined in [`src/chrome/contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/contentscript.js), sharing the same DOM and JavaScript globals as the host page (lines 21-52). This placement allows direct access to editable elements for validation but prevents direct use of Chrome extension APIs.

Background scripts run in the extension's background page or service worker as implemented in [`src/chrome/backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/backgroundscript.js) (lines 40-45). This privileged context grants full access to `chrome.runtime`, `chrome.tabs`, and `chrome.contextMenus` APIs necessary for extension-wide operations and cross-tab communication.

## Lifecycle and Persistence Models

The content script lifetime ties directly to the web page. Injected on-demand when users click the extension button or automatically for permitted sites, the script remains active only as long as the page lives and cannot rely on long-lived globals across reloads.

Background scripts follow different persistence patterns depending on the browser mode. As noted in [`backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/backgroundscript.js) lines 40-45, a background page persists for the entire browser session, while a service worker spins up only for extension events and terminates after approximately 30 seconds of inactivity. This distinction prevents reliance on global variables for persistent state in service-worker mode and requires re-initialization after restarts.

## Message Passing and Rendering Flow

Communication between these contexts occurs exclusively through `chrome.runtime.sendMessage` and `onMessage` listeners, creating a strict security boundary between page-level and extension-level operations.

When a user initiates rendering, the content script validates the focused element using `markdownHere.findFocusedElem` and `markdownHere.elementCanBeRendered`, then delegates processing via `Utils.makeRequestToPrivilegedScript` (see `requestMarkdownConversion` in [`contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/contentscript.js) lines 55-68).

The background script receives these requests through the message listener in [`src/chrome/backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/backgroundscript.js):

```javascript
// src/chrome/backgroundscript.js
chrome.runtime.onMessage.addListener(function(request, sender, responseCallback) {
  if (request.action === 'render') {
    OptionsStore.get(function(prefs) {
      responseCallback({
        html: MarkdownRender.markdownRender(
          request.mdText, prefs, marked, hljs),
        css: (prefs['main-css'] + prefs['syntax-css'])
      });
    });
    return true;   // keep channel open for async response
  }
});

```

After retrieving preferences from `OptionsStore`, the background invokes `MarkdownRender.markdownRender` and returns the HTML/CSS payload to the content script.

## State Management Responsibilities

State handling differs fundamentally between the two architectural layers. Content scripts maintain only transient UI state such as loggers and interval checks for "forgot-to-render" detection, avoiding reliance on long-lived globals since pages may reload at any moment.

Background scripts manage extension-wide configuration through `OptionsStore`, persisting user preferences across sessions. However, when operating in service-worker mode, the script must re-initialize after restarts, storing only temporary flags in memory while delegating persistent storage to the `chrome.storage` APIs.

## Content Script Injection Control

The background script determines when content scripts enter the page context through the `Injector` class:

```javascript
// src/chrome/backgroundscript.js (excerpt)
const Injector = {
  CONTENT_SCRIPTS: [
    '/common/vendor/dompurify.min.js',
    '/common/utils.js',
    '/chrome/contentscript.js'
  ],
  async injectScripts(tabId) {
    // skip if already injected
    if (await this.checkAndMarkInjected(tabId)) return true;
    for (const script of this.CONTENT_SCRIPTS) {
      await chrome.scripting.executeScript({target:{tabId}, files:[script]});
    }
    return true;
  }
};

```

This injection flow ensures that content scripts load only when necessary, first checking `checkAndMarkInjected` to avoid duplicate injections before executing the script stack via `chrome.scripting.executeScript`.

## Summary

- **Content scripts** act as lightweight, page-scoped agents in [`contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/contentscript.js) that bridge editable documents to the extension core but cannot access privileged APIs directly.
- **Background scripts** serve as the privileged hub in [`backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/backgroundscript.js) managing UI creation, persistent storage via `OptionsStore`, and the heavy-lifting Markdown rendering process.
- **Strict message passing** via `chrome.runtime` APIs maintains security boundaries, with the content script handling validation and the background script handling transformation.
- **Lifecycle differences** require distinct state strategies: transient memory for content scripts tied to page life, versus persistent storage for background scripts that must survive service-worker restarts.
- **Injection control** remains centralized in the background script, which programmatically injects content scripts including dependencies like DOMPurify only when users interact with permitted sites.

## Frequently Asked Questions

### Can content scripts access Chrome extension APIs directly in Markdown Here?

No. Content scripts execute in the web page context and cannot access extension APIs such as `chrome.runtime` or `chrome.contextMenus` directly. According to the implementation in [`src/chrome/contentscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/contentscript.js) (lines 21-52), they must communicate with the privileged background script through message passing using `chrome.runtime.sendMessage` and `onMessage` listeners to request any privileged operations.

### How does the background script process Markdown rendering requests?

When the background script receives a render action in [`src/chrome/backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/backgroundscript.js) (lines 17-30), it asynchronously retrieves user preferences from `OptionsStore`, invokes `MarkdownRender.markdownRender` with the raw Markdown text, the `marked` library, and `hljs` for syntax highlighting, then returns an object containing the rendered HTML and combined CSS strings to the requesting content script.

### What determines when content scripts are injected into a web page?

The `Injector` class defined in [`src/chrome/backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/src/chrome/backgroundscript.js) controls content script deployment. It first checks if scripts are already present using `checkAndMarkInjected`, then programmatically injects the required stack—including DOMPurify, shared utilities, and the content script itself—using `chrome.scripting.executeScript` when the user clicks the extension button or when auto-injection criteria match permitted sites.

### Why can't background scripts use global variables for persistent state in modern Chrome extensions?

Modern Chrome extensions using Manifest V3 implement background scripts as service workers that terminate after approximately 30 seconds of inactivity, as documented in [`backgroundscript.js`](https://github.com/adam-p/markdown-here/blob/main/backgroundscript.js) lines 40-45. This ephemeral lifecycle means global variables reset between events, requiring the use of `OptionsStore` or `chrome.storage` APIs for persistent state management rather than in-memory storage.