page-to-markdown

Page to Markdown

A Chrome extension that turns any web page — or just the part you selected — into clean Markdown. Copy it, or download the .md file.

No servers. No analytics. No account. It requests no host permissions at all, so it can only read a page at the moment you click it.

Install

From the Chrome Web Store: (link once published)

Or run it from source:

  1. git clone https://github.com/lakshithalk/page-to-markdown.git
  2. Open chrome://extensions and enable Developer mode
  3. Load unpacked → select the cloned folder

No build step, no dependencies, no npm install.

Use

Mode What it converts
Main content The article — nav, footer, sidebars and forms stripped out
Full page Everything in <body>, for pages with no clear structure
Selection Only what you highlighted

Also: right-click → Copy selection as Markdown, Alt+Shift+M to open, and an optional YAML front matter block (title, source URL, capture date) for Obsidian.

Headings, nested lists, tables, fenced code blocks with language detection, blockquotes, images and links all survive the trip.

Privacy

The Markdown is built in your browser and goes to your clipboard or your downloads folder. Nowhere else. The extension makes no network requests of any kind.

Two interface preferences are stored locally with chrome.storage.local and are deleted when you uninstall.

Full policy: PRIVACY.md

Permissions

Permission Why
activeTab Read the current tab’s DOM — only when you invoke the extension
scripting Inject the converter into that tab, at that moment
downloads Save the .md file you asked for
contextMenus Add the right-click entries
storage Remember the two preferences above

There is deliberately no host_permissions entry and no <all_urls>. The extension cannot run in the background or read pages you haven’t asked it to convert.

How it works

manifest.json          the contract with Chrome
src/
  background.js        service worker — context menus only
  extractor.js         DOM → Markdown. Injected on demand, never bundled.
  popup/               the panel: preview, copy, download

popup.js injects extractor.js into the active tab with chrome.scripting.executeScript, then calls the function it defined:

await chrome.scripting.executeScript({ target: { tabId }, files: ['src/extractor.js'] });
const [{ result }] = await chrome.scripting.executeScript({
  target: { tabId },
  func: (mode) => window.__pageToMarkdown(mode),
  args: [mode]
});

Two calls, because files: cannot take arguments. Both land in the same isolated world, so the second sees what the first defined. This is why activeTab suffices where a declared content script would have needed access to every site you visit.

extractor.js is a ~280-line dependency-free converter. block() handles block elements, inline() handles spans, and list() and table() are the fiddly ones. To support a new element, add a case to the relevant switch.

Development

# Convert the test fixture without opening Chrome
node test/harness.mjs article
node test/harness.mjs page

# Package for the Chrome Web Store
./build.sh

test/harness.mjs is a minimal DOM shim, not a browser — it has no CSS and no entity decoding. It catches structural regressions fast; real verification means loading test/fixture.html in Chrome and clicking the icon.

Known limits. Chrome blocks all extensions on chrome:// pages, the Web Store, and other extensions’ pages. Cross-origin iframes are unreadable. Pages that render entirely into shadow DOM come back thin.

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md.

If a page converts badly, an issue with the URL and the output you got is the most useful thing you can send.

Licence

MIT — see LICENSE. Copyright © 2026 KNOXIT Ltd.