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.
From the Chrome Web Store: (link once published)
Or run it from source:
git clone https://github.com/lakshithalk/page-to-markdown.gitchrome://extensions and enable Developer modeNo build step, no dependencies, no npm install.
| 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.
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
| 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.
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.
# 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.
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.
MIT — see LICENSE. Copyright © 2026 KNOXIT Ltd.