# HTML to Markdown Converter: Webpage to Markdown & JSON (`grace_hartwell/html-to-markdown-json`) Actor

Convert any webpage URL or raw HTML to clean, LLM-ready Markdown and JSON for RAG and AI. Removes boilerplate, keeps headings, tables and links.

- **URL**: https://apify.com/grace_hartwell/html-to-markdown-json.md
- **Developed by:** [Grace M. Hartwell](https://apify.com/grace_hartwell) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 page converteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

<img src="https://apify-image-uploads-prod.s3.us-east-1.amazonaws.com/dpsKHwXKHdbwBgwOT-profile-dxtJoi0ame-avatar-hartwell-800.png" alt="Hartwell Toolworks" width="96" height="96" />

**HTML to Markdown Converter** turns any public **webpage URL or raw HTML into clean, LLM-ready Markdown plus structured JSON**. It strips menus, cookie banners, footers and sidebars, keeps the main content (headings, lists, code blocks, tables, links) and returns the page title, meta description, language, heading outline, word count and page metadata. Use it as a **website to Markdown** tool for RAG pipelines, AI agents, content migration and SEO audits. No browser is used, so runs stay light; you pay **$1.50 per 1,000 converted pages**, and failed pages are free.

### What does HTML to Markdown Converter do?

- **Converts webpages to Markdown**: fetches each URL in your list **once** (it does not crawl further links) and converts the HTML to GitHub-flavored Markdown.
- **Converts raw HTML to Markdown**: paste one HTML document (`rawHtml`) or send many (`htmlDocuments`). Nothing is fetched in this case.
- **Extracts the main content** using Mozilla Readability (the library behind Firefox Reader View) plus extra boilerplate cleanup. If no clear article is found, it falls back to the cleaned `<body>`.
- **Returns structured JSON (HTML to JSON)** for each page: `title`, `description`, `language`, `markdown`, `wordCount`, `headings`, optional `links`, and `metadata` (canonical URL, site name, Open Graph type and image, published and modified time, meta keywords, HTTP status, content type).
- **Reports errors clearly and for free**: 404s, timeouts, login or paywall pages (401/402/403), robots.txt-blocked URLs and invalid URLs each get an error item with an `errorCode`.

### Key features

- 📝 **Clean Markdown output**: ATX headings, fenced code blocks, `-` bullet lists, absolute link URLs, optional images (`![alt](src)`).
- 📊 **HTML tables to Markdown tables** (GFM). Single-column layout tables become plain paragraphs.
- 🧹 **Boilerplate removal**: `nav`, `header`, `footer`, `aside`, forms, dialogs, buttons, hidden elements, and elements whose class or ID points to cookie, consent, banner, newsletter, sidebar, share, ads, popup, comments and similar blocks.
- 🔗 **Optional links list**: up to 500 unique links per page, each with `url`, `text` and an `internal` flag.
- 🔒 **Privacy by default**: `mailto:` and `tel:` links are reduced to plain text and never listed. E-mail addresses in the text are replaced with `[email removed]` (you can turn this off).
- 🤝 **Polite crawling**: robots.txt is respected. By default the Actor makes at most 5 parallel requests and 60 requests per minute, with a 1-second delay between requests to the same domain.
- 💸 **Budget control**: set *Max total charge* and the Actor plans its work around it and stops cleanly when the budget is reached.

### Use cases

1. **RAG and AI agents**: feed clean Markdown from documentation, blogs or knowledge bases into a vector database or an LLM prompt.
2. **LLM training and fine-tuning datasets**: collect normalized page text with `language` and `wordCount`, so you can filter by language or length.
3. **Content migration**: move HTML pages into a Markdown-based CMS, docs site, static site generator or Git repository.
4. **SEO content audits**: get titles, meta descriptions, H1–H6 heading outlines, canonical URLs and word counts for a list of URLs.
5. **Archiving and change tracking**: store readable Markdown snapshots of policy, pricing or documentation pages on a schedule.
6. **HTML clean-up**: paste messy exported HTML and get Markdown back without fetching anything.

### How much does it cost to convert HTML to Markdown?

The Actor uses **pay-per-event** pricing. You pay only for the events below; there is no separate charge for platform compute.

| Event | Price | When it is charged |
| --- | --- | --- |
| `page-converted` | **$0.0015** per page ($1.50 per 1,000) | After a converted page or HTML document is saved to your dataset |
| `apify-actor-start` | $0.00005 per run | Automatically at run start (Apify default price) |

**Not charged:** 404s, timeouts, access-restricted pages, robots.txt skips, invalid URLs and conversion errors.

**Example calculation (1,000 pages):**

```
1,000 pages × $0.0015      = $1.50
+ 1 Actor start × $0.00005 = $0.00005   (default 512 MB memory → 1 start event)
-------------------------------------------
Total                       ≈ $1.50005  → about $1.50 per 1,000 pages
```

Smaller run: 10 pages × $0.0015 = $0.015 (+ $0.00005 start).

### How to use HTML to Markdown Converter

1. Open **HTML to Markdown Converter** in Apify Store and click **Try for free** (or **Start** in Apify Console).
2. Add one or more public URLs under **URLs**, and/or paste HTML into **Raw HTML** (optionally set **Base URL for raw HTML** so relative links become absolute).
3. Choose the conversion options: main content only, keep links, keep images, convert tables, return a links list, redact e-mail addresses.
4. Optional: set **Max pages** and a **Max total charge** to cap the cost of the run.
5. Click **Start** and wait for the run to finish.
6. Open the **Overview** or **Markdown** view of the dataset, or export the results as JSON, CSV, Excel and other formats, or fetch them through the API.

### Input example

All fields are optional. Add at least one valid `startUrls` entry or some HTML in `rawHtml` / `htmlDocuments`. If all three are empty (for example an input of `{}`), the Actor converts the demo page `https://example.com/` (1 page, charged like any other page).

```json
{
    "startUrls": [
        { "url": "https://example.com" },
        { "url": "https://docs.apify.com/platform/actors" }
    ],
    "rawHtml": "<h1>Hello</h1><p>World <a href=\"/x\">link</a></p>",
    "rawHtmlBaseUrl": "https://example.org/",
    "htmlDocuments": [
        { "html": "<html lang=\"en\"><body><h2>Doc 2</h2><p>Text</p></body></html>", "url": "https://example.org/doc-2" }
    ],
    "extractMainContent": true,
    "keepLinks": true,
    "keepImages": false,
    "keepTables": true,
    "includeLinks": true,
    "redactEmails": true,
    "maxPages": 50,
    "requestTimeoutSecs": 30,
    "maxConcurrency": 5,
    "maxRequestsPerMinute": 60,
    "proxyConfiguration": { "useApifyProxy": false }
}
```

| Input field | Type | Default | Description |
| --- | --- | --- | --- |
| `startUrls` | array | – (demo: `https://example.com/` if no source is given) | Public http(s) URLs to convert. Each one is fetched once. |
| `rawHtml` | string | – | One HTML document to convert without fetching. |
| `rawHtmlBaseUrl` | string | – | Base URL used to make relative links and images in `rawHtml` absolute. |
| `htmlDocuments` | array | – | Many HTML documents: `[{ "html": "...", "url": "optional base URL" }]`. |
| `extractMainContent` | boolean | `true` | Remove boilerplate (Readability + cleanup). `false` converts the whole `<body>`. |
| `keepLinks` | boolean | `true` | Render links as `[text](url)`; off = plain text. |
| `keepImages` | boolean | `false` | Render images as `![alt](src)`. |
| `keepTables` | boolean | `true` | Convert tables to Markdown tables; off = drop tables. |
| `includeLinks` | boolean | `false` | Add a `links` array to each result. |
| `redactEmails` | boolean | `true` | Replace e-mail addresses in the Markdown and description with `[email removed]`. |
| `maxPages` | integer | `100` | Max URLs + HTML documents converted per run (1–10,000). Also capped by *Max total charge*. |
| `requestTimeoutSecs` | integer | `30` | Per-page download timeout (5–120). |
| `maxConcurrency` | integer | `5` | Max parallel downloads (1–20). |
| `maxRequestsPerMinute` | integer | `60` | Global rate limit (1–600), plus a 1 s delay per domain. |
| `proxyConfiguration` | object | `{ "useApifyProxy": false }` | Optional Apify Proxy. Not used to bypass logins or paywalls. |

### Output example

One dataset item per converted URL or HTML document (this example was produced by a local test run of the Actor):

```json
{
    "url": "https://example.com/",
    "requestedUrl": "https://example.com/",
    "source": "url",
    "status": "ok",
    "title": "Example Domain",
    "description": null,
    "language": "en",
    "markdown": "This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.",
    "wordCount": 25,
    "headings": [],
    "links": [],
    "metadata": {
        "canonicalUrl": null,
        "siteName": null,
        "ogType": null,
        "ogImage": null,
        "publishedTime": null,
        "modifiedTime": null,
        "keywords": [],
        "extractionMode": "readability",
        "markdownLength": 156,
        "statusCode": 200,
        "contentType": "text/html; charset=utf-8",
        "fetchedAt": "2026-10-25T23:01:56.939Z"
    },
    "error": null,
    "errorCode": null
}
```

Raw HTML input (`source: "html"`) produces the same shape, without `requestedUrl`, `statusCode`, `contentType` and `fetchedAt`:

```json
{
    "source": "html",
    "status": "ok",
    "url": "https://example.org/",
    "title": "Rohes HTML",
    "description": "Test ohne Abruf",
    "language": "de",
    "markdown": "# Hallo Welt\n\nEin [relativer Link](https://example.org/pfad). Kontakt: [email removed]\n\n| A | B |\n| --- | --- |\n| 1 | 2 |",
    "wordCount": 12,
    "headings": [{ "level": 1, "text": "Hallo Welt" }],
    "links": [{ "url": "https://example.org/pfad", "text": "relativer Link", "internal": true }],
    "metadata": { "canonicalUrl": null, "siteName": null, "ogType": null, "ogImage": null, "publishedTime": null, "modifiedTime": null, "keywords": [], "extractionMode": "body", "markdownLength": 121 },
    "error": null,
    "errorCode": null
}
```

Error item (free):

```json
{ "url": "https://en.wikipedia.org/wiki/This_page_does_not_exist", "source": "url", "status": "error", "errorCode": "NOT_FOUND", "error": "HTTP 404: page not found.", "metadata": { "statusCode": 404 } }
```

#### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | null | Final URL after redirects (or the base URL of an HTML document). |
| `requestedUrl` | string | URL as requested (URL sources only). |
| `source` | `url` | `html` | Where the content came from. |
| `status` | `ok` | `error` | Result status. |
| `title` | string | null | `<title>`, else `og:title` / `twitter:title`, else first `<h1>`. |
| `description` | string | null | Meta description, else `og:description` / `twitter:description`. |
| `language` | string | null | `<html lang>`, `content-language` or `og:locale`. |
| `markdown` | string | The converted Markdown. |
| `wordCount` | integer | Words in the Markdown (code blocks and URLs excluded). |
| `headings` | array | `{ level, text }` for H1–H6. |
| `links` | array | Only with `includeLinks`: `{ url, text, internal }`, max 500. |
| `metadata` | object | `canonicalUrl`, `siteName`, `ogType`, `ogImage`, `publishedTime`, `modifiedTime`, `keywords`, `extractionMode` (`readability` / `body`), `markdownLength`; for URLs also `statusCode`, `contentType`, `fetchedAt`. |
| `error`, `errorCode` | string | null | Error details; `null` on success. |

**Error codes:** `NOT_FOUND` (404/410), `ACCESS_RESTRICTED` (401/402/403), `UNAVAILABLE_FOR_LEGAL_REASONS` (451), `HTTP_ERROR`, `ROBOTS_TXT_DISALLOWED`, `TIMEOUT`, `NOT_HTML`, `FETCH_FAILED`, `INVALID_URL`, `CONVERSION_FAILED`.

### Integrations and API

Results are stored in a standard Apify dataset, so you can use everything the Apify platform offers: the Apify API, API clients, schedules, webhooks and the Apify integrations (for example Make, Zapier or n8n).

Run the Actor and get the converted pages in one HTTP call (replace `[YOUR_APIFY_TOKEN]`):

```bash
curl -X POST "https://api.apify.com/v2/acts/grace_hartwell~html-to-markdown-json/run-sync-get-dataset-items?token=[YOUR_APIFY_TOKEN]" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":[{"url":"https://example.com"}],"maxPages":10}'
```

With the JavaScript API client:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('grace_hartwell/html-to-markdown-json').call({
    startUrls: [{ url: 'https://example.com' }],
    includeLinks: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0].markdown);
```

### FAQ

#### Does the HTML to Markdown Converter render JavaScript?

No. It parses the HTML the server returns, without a browser. Pages that build their content only in the browser (client-side rendering) may come back short or empty. Check `wordCount` to spot them.

#### Can it crawl a whole website to Markdown?

No. Each URL you provide is converted once, and links are not followed. To convert a whole site, pass in a list of URLs (for example from the site's sitemap).

#### How does main content extraction work?

It uses Mozilla Readability with extra boilerplate cleanup. If Readability finds less than about 120 characters of article text, the cleaned `<body>` is converted instead. You can see which method was used in `metadata.extractionMode` (`readability` or `body`). Set `extractMainContent: false` to always convert the full body.

#### Can I convert raw HTML to Markdown without a URL?

Yes. Use `rawHtml` for one document or `htmlDocuments` for many. Nothing is fetched, and the optional base URL is used only to make relative links absolute.

#### Which pages are free?

Only successfully converted pages are charged. Errors (404, 401/402/403, robots.txt-disallowed, timeouts, non-HTML responses, invalid URLs, conversion failures) are returned as free error items.

#### Can it get past logins, paywalls or bot protection?

No. It never logs in, never sends cookies and never rotates sessions to get around 401/402/403 responses. Those pages are reported as `ACCESS_RESTRICTED`.

#### Why do some table cells look flattened?

Markdown tables can't hold nested HTML, so each cell is converted to plain text (with pipes escaped). Turn off `keepTables` to drop tables completely.

#### Does it extract e-mail addresses or phone numbers?

No. `mailto:` and `tel:` links are never listed, and e-mail addresses in the text are redacted by default. If a page you choose contains personal data in its body text, you are responsible for having a legal basis (for example under GDPR) to process it.

### Limitations: what this Actor does not do

- **No JavaScript rendering**: it uses a plain HTTP client (Cheerio), not a headless browser.
- **No crawling**: links on a page are not followed. Duplicate URLs in the input are converted once.
- **No logins, cookies, paywall or anti-bot bypass.** Proxies are optional and not meant for circumvention.
- **Only HTML pages are converted**: PDFs, images and other non-HTML files are not converted and come back as free error items (`NOT_HTML` or `FETCH_FAILED`).
- **Images are not downloaded**: with `keepImages` on, only `![alt](src)` references are kept. `<figcaption>` text, `<iframe>`, `<svg>`, `<canvas>`, scripts and styles are dropped.
- **Tables are simplified**: nested markup inside cells becomes plain text, and single-column tables become paragraphs.
- **Links list limit**: at most 500 links per page; icon-only links (no text), `mailto:`/`tel:` links and non-http(s) links are skipped.
- **Heuristic boilerplate removal**: elements with class or ID names like `related`, `comments`, `share` or `sidebar` are removed, which can occasionally remove wanted content. Turn off `extractMainContent` if that happens.
- **No author names, e-mails or phone numbers** are extracted into fields.
- **Each run is capped** by `maxPages` (max 10,000) and by your *Max total charge*.

***

Author: Grace M. Hartwell

# Actor input Schema

## `startUrls` (type: `array`):

Public web pages to convert. Each URL is fetched once (no crawling of further links). robots.txt is respected. If you leave URLs, raw HTML and HTML documents all empty, the Actor converts the demo page https://example.com/.

## `rawHtml` (type: `string`):

Optional: paste an HTML document to convert it without fetching anything.

## `rawHtmlBaseUrl` (type: `string`):

Optional: used to turn relative links/images in the raw HTML into absolute URLs.

## `htmlDocuments` (type: `array`):

Optional: array of objects like {"html": "<html>...", "url": "https://optional-base-url"}.

## `extractMainContent` (type: `boolean`):

Remove navigation, headers, footers, cookie banners, sidebars and other boilerplate (Mozilla Readability + cleanup). Turn off to convert the whole <body>.

## `keepLinks` (type: `boolean`):

Render hyperlinks as [text](url). Off = plain text only.

## `keepImages` (type: `boolean`):

Render images as ![alt](src).

## `keepTables` (type: `boolean`):

Convert HTML tables to Markdown (GFM) tables. Off = drop tables.

## `includeLinks` (type: `boolean`):

Add a structured `links` array (url, text, internal) to each result. mailto:/tel: links are never included.

## `redactEmails` (type: `boolean`):

Replace e-mail addresses in the Markdown with \[email removed]. Recommended (privacy).

## `maxPages` (type: `integer`):

Maximum number of URLs + HTML documents converted in this run. Also capped by your 'Max total charge' budget.

## `requestTimeoutSecs` (type: `integer`):

Per-page download timeout.

## `maxConcurrency` (type: `integer`):

Maximum parallel downloads.

## `maxRequestsPerMinute` (type: `integer`):

Global rate limit. Additionally there is a 1 s delay between requests to the same domain.

## `proxyConfiguration` (type: `object`):

Optional. Not needed for most public pages. Proxies are NOT used to bypass logins or paywalls.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://example.com"
    }
  ],
  "extractMainContent": true,
  "keepLinks": true,
  "keepImages": false,
  "keepTables": true,
  "includeLinks": false,
  "redactEmails": true,
  "maxPages": 100,
  "requestTimeoutSecs": 30,
  "maxConcurrency": 5,
  "maxRequestsPerMinute": 60,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://example.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("grace_hartwell/html-to-markdown-json").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "startUrls": [{ "url": "https://example.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("grace_hartwell/html-to-markdown-json").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://example.com"
    }
  ]
}' |
apify call grace_hartwell/html-to-markdown-json --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,grace_hartwell/html-to-markdown-json"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/BC5rdBAPz4ghEBxMO/builds/Y4nEq6224hQehcqjc/openapi.json
