Claude
Skills
Sign in
Back

md-this-page-extension

Included with Lifetime
$97 forever

```markdown

Writing & Docs

What this skill does

```markdown
---
name: md-this-page-extension
description: Browser extension that converts any webpage to clean, LLM-ready Markdown using Mozilla Readability and Turndown
triggers:
  - convert webpage to markdown
  - md this page extension
  - html to markdown browser extension
  - extract page content as markdown
  - build chrome extension with plasmo
  - webpage markdown converter
  - readability turndown extension
  - llm ready markdown from webpage
---

# MD This Page Extension

> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.

A Chrome/Firefox browser extension built with Plasmo + React that converts any webpage into clean, structured Markdown in one click. Uses Mozilla's Readability for content extraction and Turndown for HTML-to-Markdown conversion — optimized for LLM workflows.

---

## What It Does

- Strips navigation, ads, scripts, and boilerplate from any webpage
- Extracts the main content using `@mozilla/readability`
- Converts extracted HTML to Markdown using `turndown`
- Opens a preview tab with copy/download/prompt-copy options
- Supports toggling images, links, metadata, source URL, and page structure output

---

## Tech Stack

| Tool | Purpose |
|------|---------|
| [Plasmo](https://docs.plasmo.com/) | Browser extension framework |
| React | UI |
| Tailwind CSS | Styling |
| `@mozilla/readability` | Content extraction |
| `turndown` | HTML → Markdown |

---

## Installation & Development Setup

### Prerequisites
- Node.js 18+
- pnpm

### Clone & Install

```bash
git clone https://github.com/Ademking/MD-This-Page.git
cd MD-This-Page
pnpm install
```

### Development (Chrome)

```bash
pnpm dev
# Generates: build/chrome-mv3-dev/
```

Load in Chrome:
1. Navigate to `chrome://extensions/`
2. Enable **Developer mode**
3. Click **Load unpacked**
4. Select `build/chrome-mv3-dev`

### Development (Firefox)

```bash
pnpm dev --target=firefox-mv2
# Generates: build/firefox-mv2-dev/
```

### Production Build

```bash
pnpm build
# Output: build/chrome-mv3-prod/

pnpm build --target=firefox-mv2
# Output: build/firefox-mv2-prod/
```

---

## Project Structure

```
md-this-page/
├── background/
│   └── index.ts          # Service worker: context menu, keyboard shortcut
├── contents/
│   └── extractor.ts      # Content script: Readability extraction
├── tabs/
│   └── preview.tsx       # Preview tab UI (React)
├── components/           # Shared React components
├── utils/
│   └── turndown.ts       # Turndown configuration/helpers
├── assets/               # Icons, SVGs
├── package.json
└── plasmo.config.ts      # Plasmo configuration
```

---

## Key Code Patterns

### 1. Content Script: Extracting Page Content with Readability

```typescript
// contents/extractor.ts
import { Readability } from "@mozilla/readability";

export function extractPageContent(): {
  title: string;
  content: string;
  author: string | null;
  publishedTime: string | null;
  url: string;
} {
  // Clone document so Readability doesn't mutate the live DOM
  const documentClone = document.cloneNode(true) as Document;
  const reader = new Readability(documentClone);
  const article = reader.parse();

  return {
    title: article?.title ?? document.title,
    content: article?.content ?? document.body.innerHTML,
    author: article?.byline ?? null,
    publishedTime: article?.publishedTime ?? null,
    url: window.location.href,
  };
}
```

### 2. Converting HTML to Markdown with Turndown

```typescript
// utils/turndown.ts
import TurndownService from "turndown";

export interface ConversionOptions {
  keepImages: boolean;
  keepLinks: boolean;
  includeMetadata: boolean;
  includeSourceUrl: boolean;
  generatePageMap: boolean;
}

export function htmlToMarkdown(
  html: string,
  options: ConversionOptions
): string {
  const turndownService = new TurndownService({
    headingStyle: "atx",      // # H1, ## H2 style
    codeBlockStyle: "fenced", // ```code``` style
    bulletListMarker: "-",
  });

  // Optionally strip images
  if (!options.keepImages) {
    turndownService.addRule("removeImages", {
      filter: "img",
      replacement: () => "",
    });
  }

  // Optionally strip links (keep text only)
  if (!options.keepLinks) {
    turndownService.addRule("removeLinks", {
      filter: "a",
      replacement: (content) => content,
    });
  }

  return turndownService.turndown(html);
}

export function buildFullMarkdown(
  extracted: { title: string; content: string; author: string | null; publishedTime: string | null; url: string },
  options: ConversionOptions
): string {
  const lines: string[] = [];

  if (options.includeMetadata) {
    lines.push(`# ${extracted.title}`);
    if (extracted.author) lines.push(`**Author:** ${extracted.author}`);
    if (extracted.publishedTime) lines.push(`**Published:** ${extracted.publishedTime}`);
    lines.push("");
  }

  if (options.includeSourceUrl) {
    lines.push(`**Source:** ${extracted.url}`);
    lines.push("");
  }

  const markdown = htmlToMarkdown(extracted.content, options);
  lines.push(markdown);

  return lines.join("\n");
}
```

### 3. Background Service Worker: Context Menu & Shortcut

```typescript
// background/index.ts
import { sendToContentScript } from "@plasmohq/messaging";

// Register context menu item
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "md-this-page",
    title: ".MD this page",
    contexts: ["page", "selection"],
  });
});

// Handle context menu click
chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "md-this-page" && tab?.id) {
    triggerExtraction(tab.id);
  }
});

// Handle keyboard shortcut (Alt+M defined in manifest)
chrome.commands.onCommand.addListener((command) => {
  if (command === "trigger-md") {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]?.id) triggerExtraction(tabs[0].id);
    });
  }
});

async function triggerExtraction(tabId: number) {
  // Execute content script to extract content
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => {
      // This runs in page context — calls extractPageContent()
      return window.__mdThisPage?.extract();
    },
  });

  const data = results?.[0]?.result;
  if (!data) return;

  // Open preview tab with extracted data
  chrome.storage.session.set({ extractedContent: data }, () => {
    chrome.tabs.create({
      url: chrome.runtime.getURL("tabs/preview.html"),
    });
  });
}
```

### 4. Preview Tab: React UI

```tsx
// tabs/preview.tsx
import { useEffect, useState } from "react";
import { buildFullMarkdown, ConversionOptions } from "../utils/turndown";

const DEFAULT_OPTIONS: ConversionOptions = {
  keepImages: true,
  keepLinks: true,
  includeMetadata: true,
  includeSourceUrl: true,
  generatePageMap: false,
};

export default function PreviewTab() {
  const [markdown, setMarkdown] = useState("");
  const [options, setOptions] = useState<ConversionOptions>(DEFAULT_OPTIONS);
  const [extracted, setExtracted] = useState(null);

  useEffect(() => {
    chrome.storage.session.get("extractedContent", ({ extractedContent }) => {
      if (extractedContent) {
        setExtracted(extractedContent);
        setMarkdown(buildFullMarkdown(extractedContent, options));
      }
    });
  }, []);

  useEffect(() => {
    if (extracted) {
      setMarkdown(buildFullMarkdown(extracted, options));
    }
  }, [options, extracted]);

  const handleCopy = () => {
    navigator.clipboard.writeText(markdown);
  };

  const handleDownload = () => {
    const blob = new Blob([markdown], { type: "text/markdown" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${extracted?.title ?? "page"}.md`;
    a.click();
    URL.revokeObjectURL(url);
  };

  const handleCopyAsPrompt = () => {
    const prompt = `Please analyze the following content:\n\n${markdown}`;
    navigator.clipboard.writeText(prompt);
  };

Related in Writing & Docs