# Website to Markdown Crawler - Content Crawler for LLM & RAG (`tidytools/website-markdown-crawler`) Actor

Website to Markdown crawler for RAG and LLMs: crawl a website or docs site into clean Markdown (URL to Markdown, HTML to Markdown) with RAG chunks. Sitemaps, JavaScript pages, linked PDFs and Word. $1/1k pages.

- **URL**: https://apify.com/tidytools/website-markdown-crawler.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** AI, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 3 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 page (fast mode)s

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### What does Website to Markdown Crawler do?

**Website to Markdown Crawler converts a website to Markdown for RAG: give it a start URL (a docs site, help center or blog) and get clean, LLM-ready Markdown for every page, RAG chunks with heading paths and one combined `OUTPUT.md`, for $1 per 1,000 pages ($2.50 when a real browser is needed), compute included. Failed, blocked and duplicate pages are free.**

#### How to convert a website to Markdown for RAG

1. Enter the site in **Start URLs**, one per line (for example `https://docs.example.com/` or just `docs.example.com`). A new Console input is prefilled with **Max pages: 10** for a small first run; you can raise **Max pages** yourself. If an API input omits `maxPages`, the default is still 50.
2. Optional: set **RAG chunk size (characters)**, for example 2000.
3. Click **Start**. Each page becomes one row with `markdown` and `chunks`; `OUTPUT.md` holds the whole site in one file. A page that cannot be read gets a free row with an `error` and a `hint` saying what to try (see [FAQ](#faq)).
4. Load the chunks into your vector store (LangChain and LlamaIndex examples below) or fetch them via API. A 1,000-page docs site costs $1 in fast mode.

#### Why this one

- **A price you know before the run.** Apify's free Website Content Crawler (WCC) bills compute time plus Apify Proxy instead: its README estimates about $0.20 per 1,000 pages over plain HTTP and $0.50–$5 with a browser (checked September 2026), and its default settings cost $1.55–$3.65 per 1,000 pages in our benchmark. A tuned WCC run can still cost less on large plain-HTTP sites ([comparison](#compared-with-apifys-website-content-crawler)).
- **Keeps the body text.** In our 5-site test (25 pages each, default settings) this Actor kept at least 97% of the sampled body paragraphs on every site and took 3–34 seconds per site; WCC took 56–200 seconds and kept 0% on a Next.js docs site and 34% on an Intercom help center ([benchmark](https://tools.yukai.uk/benchmarks/markdown-vs-wcc)).
- **Linked PDF, Word, Excel and CSV files are converted** to Markdown (WCC can save the files, but does not convert them).
- **RAG chunks with heading paths** and one combined `OUTPUT.md` file (llms-full.txt style) in the same run, at no extra charge.
- **No proxy or API key to set up**: pages are fetched from our servers first, and a real browser is used only when a page needs it.

#### Input (copy and paste)

```json
{
    "urls": ["https://docs.apify.com/academy"],
    "maxPages": 10,
    "chunkSize": 2000
}
```

#### Output (real result, shortened)

One page from a test run on `docs.apify.com/academy`, read in fast mode and charged $0.001. For the 10-page limit in the first-run input above, 10 successfully converted pages cost at most $0.01 if all use fast mode, or at most $0.025 if all use browser mode. Actual cost depends on the number of successful pages and each page's mode; pages that fail to convert are free. The crawl may find fewer than 10 pages:

```json
{
    "url": "https://docs.apify.com/academy/actor-marketing-playbook/actor-basics/actor-description",
    "startUrl": "https://docs.apify.com/academy",
    "title": "Actor description & SEO description | Academy | Apify Documentation",
    "mode": "fast",
    "success": true,
    "charged": true,
    "wordCount": 1205,
    "markdown": "# Actor description & SEO description\n\nCopy for LLM\n\nLearn about Actor description and meta description. …\n\n## What is an Actor description?\n\n…",
    "contentDigestKind": "markdown:v1",
    "contentDigest": "sha256:…",
    "chunks": [
        { "index": 1, "text": "… ### Is there any benefit in the description and meta description being different?\n\n…", "headingPath": ["Actor description & SEO description", "Regular description vs. SEO description", "Is there any benefit in the description and meta description being different?"], "charCount": 1850 }
    ],
    "crawledAt": "2026-09-29T03:56:19.278Z"
}
```

All fields, the failed-page row and the `SUMMARY` record: [Output example](#output-example).

### How much does it cost?

Pay per event, only for pages that were converted successfully:

| Event | Price |
|---|---|
| Page (fast mode, plain HTTP) | $1.00 / 1,000 pages |
| Page (browser mode, JavaScript rendering) | $2.50 / 1,000 pages |

**Example:** a 2,000-page documentation site costs **$2.00** when every page is read in fast mode, and at most **$5.00** if every page needed the browser.

**No start fee.** Compute time is included: the table is the whole price. In **Auto** mode (default) most pages use the fast mode. Duplicates, blocked pages (403, bot checks), pages disallowed by robots.txt, pages with no text even after the browser fallback, and errors are not charged. Your maximum charge limit is always respected. On Apify's Scale plan prices are 10% lower, on Business and higher 20% lower ($0.80 / 1,000 fast pages).

### Features

- 📄 **Documents too**: linked **PDF, Word (DOCX), Excel (XLSX) and CSV** files are converted to Markdown, not just downloaded
- ✂️ **RAG chunks built in**: optional `chunks` array split at paragraph boundaries with overlap, each with its `headingPath` (h1 > h2 > h3) and `charCount`, ready for embeddings
- 📚 **One-file export**: every converted page also goes into one `OUTPUT.md` file in crawl order (llms-full.txt style), each page under a `# Title` header with a `Source:` line, ready to paste into an LLM. Its link is `combinedFileUrl` in the `SUMMARY` record. Free, on by default
- 💵 **Fixed price per page**: **$1 per 1,000 pages** over plain HTTP, $2.50 when a real browser is needed. Compute is included, and failed, blocked and duplicate pages are **free**
- 🧹 **Clean Markdown by default**: menus, footers, sidebars, related-post and newsletter blocks are removed; code fences, tables and absolute links are kept. Add your own CSS selectors to remove more, or keep only one part of the page (details under **What gets removed** in Tips)
- 🌐 **Many sites in one run**: paste start URLs one per line (bare domains are fine). Every page has `startUrl`, so you can group results by site, **Max pages per start URL** gives each site its share, and the `SUMMARY` record has one entry per site
- 🕷️ **Crawls for you**: start from one URL and follow links within the same folder, the whole site, or only the pages you list; the site's `sitemap.xml` is read too, so pages behind JavaScript menus are not missed. Links inside page content are crawled first; other languages, old versions and author/tag listing pages come last, so a small **Max pages** goes to real content
- 🤖 **Respects robots.txt** by default (can be turned off for your own sites)
- 🏷️ **Page metadata**: `description`, `language`, `canonicalUrl` and `wordCount` for every page
- 🔤 **Markdown or plain text**: choose one or both
- ⚡ **Real browser only when needed**: JavaScript-only pages and sites that block simple requests are rendered in headless Chrome automatically

### Who is it for?

- Building a **RAG chatbot** or **AI assistant** over your docs, help center or website
- Feeding **LLM agents** with up-to-date documentation
- Creating a **knowledge base export** or content audit
- **Migrating** a website's content to another CMS

### Price comparison with other crawlers

#### Compared with Apify's Website Content Crawler

Apify's free **Website Content Crawler** (WCC) is the most used crawler on the Store and has more options than this Actor (several HTML-to-Markdown converters, logins, expanding hidden content, HTML output). You pay for its compute time instead of per page. Its own README estimates about **$0.20 per 1,000 pages** with the plain HTTP crawler and **$0.50–$5 per 1,000 pages** with a browser, depending on the site, plus Apify Proxy (checked September 2026).

**Our benchmark** (5 sites, 25 pages each, both tools on the settings a new user gets, September 2026): this Actor took 3–34 seconds per site and WCC 56–200 seconds; WCC's default run cost about $1.55–$3.65 per 1,000 pages in platform usage on our account. WCC's defaults lost most of the body text on a Next.js docs site (0 of 82 sampled paragraphs) and an Intercom help center (34%); this Actor kept at least 97% on all five sites. WCC was still cleaner on blog pages (fewer leftover footer and related-post lines). Method and per-run numbers: [benchmark page](https://tools.yukai.uk/benchmarks/markdown-vs-wcc).

So for large sites that work over plain HTTP, a tuned WCC run can cost less than this Actor. Choose this Actor when you want:

- a **price you know before the run**: $1 or $2.50 per 1,000 pages, and nothing for failed, blocked or duplicate pages;
- **linked PDF, Word and Excel files converted** to Markdown (WCC can save the files, but does not convert them);
- **RAG chunks with heading paths** in the same run.

Already crawled a site with WCC? [Website to Vector Embeddings](https://apify.com/tidytools/website-to-embeddings) can embed its dataset directly.

Other paid crawlers on the Apify Store, per 1,000 pages (checked September 2026): worshipful_knife Website Content Crawler $1.00 (Business plan $0.70) plus platform usage and a start fee; joyouscam35875 Website Content Crawler free today, $2.00 from October 5, 2026; datascoutapi Website Content Crawler Pro $2.99; easyapi Website Content to Markdown $2.99 plus a $0.09 start fee and platform usage; 6sigmag Fast Website Content Crawler $3.00 plus a start fee; parseforge Website Content Crawler $20–$25.

| | This Actor |
|---|---|
| Fixed price per page, compute included | ✅ |
| PDF, Word and Excel links converted | ✅ |
| RAG chunks with heading paths | ✅ |
| Navigation and footer removal | ✅ |
| JavaScript pages (real browser when needed) | ✅ |
| Sitemap seeding and robots.txt | ✅ |
| Duplicates and failed pages free | ✅ |

### Control your cost

- **What is charged:** each page converted successfully, at the fast or the browser price.
- **What is free:** failed and blocked pages, pages with no text (`errorType: "no_text"`), duplicate pages, pages disallowed by robots.txt and invalid input lines. Every row has `charged: true` or `false`.
- **Max pages** caps the whole run; with several start URLs, **Max pages per start URL** stops one big site from using it all.
- At the start, the run shows an estimate in the log and the run's status message, e.g. `Estimate: up to 50 page(s) x $0.001 (fast) to $0.0025 (browser) per page = at most $0.125 (about $0.05 if pages read in fast mode). You pay only for pages actually returned; failed, empty and duplicate pages are free.`
- It also logs its worst case, e.g. `Plan: 50 pages × $0.0025 = at most $0.125` (every page at the browser price), and warns when that is more than your **maximum charge per run**.
- The log also shows the expected cost, and the `SUMMARY` record has both: `maxCostUsd` (the most this run can cost: every planned page at the browser price, or the fast price in Fast mode, capped at your maximum charge per run) and `expectedCostUsd` (every planned page in fast mode, the usual case).
- When the maximum charge per run is reached, the run stops and keeps all pages converted so far. The status message says so, and the `SUMMARY` record has `status: "LIMIT_REACHED"` and `notProcessed` (how many queued pages were not crawled, and up to 100 of them).

### From a test run to the whole site

Start with a small run (10-20 pages). If it converts at least one page and stops only because it reached **Max pages** or **Max pages per start URL** while more pages were found, the `SUMMARY` record in the run's key-value store has a `nextStep` object. For a 20-page run with a $5 maximum charge per run in Auto mode it looks like this:

```json
{
    "nextStep": {
        "message": "Small crawl succeeded (20 pages converted). To scale up, set Crawl scope to \"Whole website\", raise Max pages to 2000; estimated cost at most $5 for up to 2000 pages.",
        "suggestedInput": { "crawlScope": "domain", "maxPages": 2000 },
        "estimatedCostUsd": 5
    }
}
```

- `suggestedInput` lists only the fields to change: `crawlScope` set to `domain` (Whole website), `maxPagesPerStartUrl` set back to `0` (no per-start-URL limit) if you had set one, and `maxPages` set to the number of pages your budget covers. Copy these values into a new run.
- `estimatedCostUsd` is the most the new run can cost: Auto and Browser mode count every page at the browser price ($2.50 per 1,000), Fast mode at the fast price ($1 per 1,000). If you set a maximum charge per run, `maxPages` is kept within it; without one, `maxPages` is 10,000 (the highest allowed), so the estimate is $25 in Auto mode. You pay only for pages actually converted; failed, blocked and duplicate pages are free.
- There is no `nextStep` when nothing was left to crawl, when no page was converted, when the run stopped because of the memory or maximum charge limit, or when the suggested values would not change your input.

### How to use it

1. Enter one or more **Start URLs**, one per line, e.g. `https://docs.example.com/` or just `docs.example.com`.
2. Choose the **Crawl scope** (same folder is best for documentation).
3. Set **Max pages**.
4. Optional: keep **Remove navigation, headers and footers** on (the default), set a **Content selector** such as `main` or `article`, URL include/exclude patterns, **Output formats**, and a **RAG chunk size** (e.g. 2000).
5. Optional: **Use the site's sitemap** is on (*Seed*) by default; choose *Only* to convert just the sitemap's pages, or *Off* to follow links only.
6. Click **Start** and download the results as JSON, CSV or Excel, or fetch them via API.

#### Input example

```json
{
    "urls": ["https://docs.apify.com/academy"],
    "crawlScope": "path",
    "maxPages": 200,
    "maxPagesPerStartUrl": 0,
    "mode": "auto",
    "removeBoilerplate": true,
    "removeSelectors": ".related-posts, #comments",
    "excludeUrlPatterns": ["**/changelog/**"],
    "useSitemap": "seed",
    "respectRobotsTxt": true,
    "outputFormats": ["markdown", "text"],
    "chunkSize": 2000,
    "chunkOverlap": 200
}
```

#### Output example

Real output from a test run on `https://docs.apify.com/academy` with **Remove navigation, headers and footers** on, plain text and 2,000-character chunks (long text shortened with …):

```json
{
    "url": "https://docs.apify.com/academy/actor-marketing-playbook/actor-basics/actor-description",
    "finalUrl": "https://docs.apify.com/academy/actor-marketing-playbook/actor-basics/actor-description",
    "startUrl": "https://docs.apify.com/academy",
    "title": "Actor description & SEO description | Academy | Apify Documentation",
    "depth": 0,
    "httpStatus": 200,
    "contentType": "text/html; charset=utf-8",
    "mode": "fast",
    "via": "backend",
    "success": true,
    "charged": true,
    "description": "Learn about Actor description and meta description. Where to set them and best practices for both content and length.",
    "language": "en",
    "canonicalUrl": "https://docs.apify.com/academy/actor-marketing-playbook/actor-basics/actor-description",
    "wordCount": 1205,
    "foundIn": "sitemap",
    "markdown": "# Actor description & SEO description\n\nCopy for LLM\n\nLearn about Actor description and meta description. …\n\n## What is an Actor description?\n\n…",
    "text": "Actor description & SEO description\n\nCopy for LLM\n\nLearn about Actor description and meta description. …",
    "contentDigestKind": "markdown:v1",
    "contentDigest": "sha256:…",
    "chunks": [
        { "index": 1, "text": "… ### Is there any benefit in the description and meta description being different?\n\n…", "headingPath": ["Actor description & SEO description", "Regular description vs. SEO description", "Is there any benefit in the description and meta description being different?"], "charCount": 1850 }
    ],
    "crawledAt": "2026-09-29T03:56:19.278Z"
}
```

Without the clean-up option, pages on this site start with `[Skip to main content](#…)` and `On this page`, and end with the page's table-of-contents links. On `docs.apify.com/academy/web-scraping-for-beginners` the option cut the Markdown from 7,612 to 7,108 characters by removing exactly those parts.

- `via` says where the page was fetched from: `backend` (our servers), `direct` (Apify's network or your proxy) or `browser`.
- `foundIn` is `start`, `sitemap` or `link`.
- `startUrl` is the start URL the page was reached from. `url` is the address requested, kept as you entered it for start pages (a trailing `/` is not removed); start pages also have `inputUrl`, the line exactly as you typed it.
- `description`, `language` and `canonicalUrl` are `null` when the page does not declare them (and may be `null` for pages rendered in the browser).
- `text` appears only when **Output formats** includes *Plain text*; `markdown` is `null` if you leave Markdown out.
- `contentDigestKind` and `contentDigest` are a SHA-256 digest of this row's own output text, so you can tell whether two rows carry the same content, or diff two crawls of the same site, without re-reading the pages. `contentDigest` is `sha256:` plus the hex digest of the UTF-8 bytes of `<contentDigestKind>\0<field value>`, with no other normalisation: the same text always gives the same digest, and one changed character gives a different one. `contentDigestKind` is `markdown:v1` (the digest is of the `markdown` field) when Markdown output is on, and `text:v1` (the `text` field) when you leave Markdown out; when the row has both, the digest is of `markdown`.
- The digest is of this row's own output text only — not of `OUTPUT.md`, the page's original HTML, or the fetch time — so it changes only when the row's content changes. Failed pages and pages with no text have no digest fields (see the failed-page row below).
- Tip: turn on **Remove navigation, headers and footers**, or set **Content selector** to `main` or `article`, to drop "Skip to main content" and menu text.

A failed page is not charged and looks like this:

```json
{ "url": "https://example.com/missing", "startUrl": "https://example.com/", "depth": 1, "success": false, "error": "target returned HTTP 404 (not charged)", "errorType": "not_found", "hint": "The page does not exist (HTTP 404/410). Check the start URL for typos, …", "routesTried": ["backend"], "charged": false }
```

The run's key-value store has a `SUMMARY` record with `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS`, `LIMIT_REACHED` or `INVALID_INPUT`), page counts, `disallowedByRobotsTxt`, `sitemapUrlsQueued`, `emptyPagesNotCharged`, `sites` (one entry per start URL: `pages`, `failed`, `duplicates`, `notCrawled`), the cost estimate `maxCostUsd` and `expectedCostUsd`, and `combinedFileUrl`: the link to `OUTPUT.md`, all pages in one Markdown file. Very large crawls split that file into 8 MB parts (`OUTPUT-2.md`, …) listed in `combinedFileUrls`. Turn it off with **Also save one combined Markdown file** (`combinedFile: false`).

### Find specific information with a goal

Looking for one thing on a site (pricing, a refund policy, API rate limits)? Add a **Goal** in a few words. Among the start URLs, sitemap pages and links found on a page, those whose link text or URL path contain these words are crawled first (a matching menu link counts as a content link), so even a small **Max pages** reaches the answer page. Each page row then has `relevantBlocks`: up to 5 sections of the page that match the goal best (cut at its headings), each with `headingPath`, `text` (up to 1,500 characters), `startIndex` (where `text` starts in the `markdown` field) and `score`; `[]` when nothing on the page matches. SUMMARY has `goalTopPages`: the 5 best-matching pages with their `score`.

```json
{ "urls": ["https://example.com"], "crawlScope": "domain", "maxPages": 10, "goal": "pricing plans" }
```

It is plain word matching (no AI model, English stop words ignored), so use the words the site would use. The price per page is the same, Max pages, Max link depth, URL patterns and robots.txt work as before, and without a goal the crawl and its output are unchanged.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/website-markdown-crawler) to Claude, Cursor or any MCP client, then ask e.g. "Crawl the first 20 pages of docs.apify.com/academy and summarize what the course covers."

Minimal input:

```json
{ "urls": ["https://docs.apify.com/academy"], "maxPages": 20 }
```

Failed items are not charged and carry an `errorType`.

### Use it from code

Example with the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/tidytools~website-markdown-crawler/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://docs.example.com"],"maxPages":20}'
```

#### LangChain (Python)

```python
from langchain_apify import ApifyWrapper
from langchain_core.documents import Document

loader = ApifyWrapper().call_actor(
    actor_id="tidytools/website-markdown-crawler",
    run_input={"urls": ["https://docs.apify.com/academy"], "maxPages": 20},
    dataset_mapping_function=lambda item: Document(
        page_content=item.get("markdown") or "", metadata={"source": item["url"], "title": item.get("title")}
    ),
)
docs = loader.load()
```

#### LlamaIndex (Python)

```python
from apify_client import ApifyClient
from llama_index.core import Document

client = ApifyClient("YOUR_TOKEN")
run = client.actor("tidytools/website-markdown-crawler").call(
    run_input={"urls": ["https://docs.apify.com/academy"], "maxPages": 20})
docs = [Document(text=i["markdown"], metadata={"url": i["url"], "title": i.get("title")})
        for i in client.dataset(run["defaultDatasetId"]).iterate_items() if i.get("success")]
```

**Schedules and integrations:** run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.

### Tips

- **Documentation sites**: keep the scope on *Same folder* and keep **Remove navigation, headers and footers** on. If the start page links to nothing inside its own folder (sites that give every page its own folder, such as `/en/getting-started/`), the crawl uses the folder one level up for it (`/en/`) and lists it in SUMMARY `scopeWidened`. If the start URL redirects to another host or folder (a docs site that moved, e.g. `docs.example.io/en/stable/` to `docs.example.com/platform/stable/`), links under the redirect target are crawled as well and it is listed in SUMMARY `scopeRedirected`. If some menu text is left, add its selector to **Also remove these elements** or set the content selector to `main` or `article`.
- **Calling the API?** Clean-up and tidying are on unless you send `"removeBoilerplate": false` or `"tidyMarkdown": false`. Use the plain `urls` list: the older `startUrls` field still works, but Apify rejects the whole run (HTTP 400) when it holds a bare domain or a blank line.
- **Several sites at once**: set **Max pages per start URL** (e.g. 20) so each site gets pages, and group the results by `startUrl`.
- **JavaScript-heavy apps** (React, Vue, Angular): Auto mode switches to the browser automatically; choose *Browser* to force it.
- **Only part of a site**: use include patterns like `https://example.com/blog/**`. Start URLs are always converted, even when they do not match the patterns.
- **Big sites**: raise *Max pages* and *Parallel pages*; you only pay for pages converted. Memory use is bounded: in our test a 2,700-page crawl peaked at about 83 MB, so the default 256 MB covers roughly 5,000 pages; for 10,000 pages set the run's memory to 512 MB. If a crawl would not fit, the log warns at the start, and if memory still runs short the run stops cleanly with the pages done so far (`SUMMARY.status` is `LIMIT_REACHED`, `stoppedEarly.reason` is `memory`); run again with more memory for the rest. In browser mode, pages that find no free browser ("no browser became available") are retried twice after a short wait before they count as failed (free).
- **Only the pages in the sitemap**: set **Use the site's sitemap** to *Only*.
- **A site blocks our servers**: under **Advanced settings**, *Auto* already retries from Apify's network; add an Apify proxy if it still fails (proxy usage is billed to your Apify account).

#### What gets removed

With `removeBoilerplate` on, this also removes hidden elements (`hidden` or inline `display:none`) and schema.org JSON-LD blocks before Markdown conversion. A hidden wrapper that holds most of the page (some shop themes reveal the whole page with JavaScript) is kept, and so is the `<noscript>` crawler view of a page that is otherwise empty without JavaScript (Discourse forums).
It also strips tracking parameters (`utm_*` and others), empty links, and duplicate links whose text is the URL. With `removeBoilerplate` off, this link cleanup does not change the output.

**Remove navigation, headers and footers** drops menus, footers, sidebars, breadcrumbs and "Skip to main content" links, and **Tidy the Markdown** removes skip links left at the top ("[Skip to content](#…)"), leftover `<!doctype>` lines, empty headings, tracking pixels, schema.org JSON-LD blocks and sections repeated word for word (e.g. a page rendered twice for desktop and mobile). When a page has one clear main content element (`<article>`, `role="main"` or `<main>`), only that part is converted, so "Was this helpful?" widgets, theme pickers and related-post blocks around it stay out. Code blocks keep their fences (also on Sphinx/Pygments sites), links are absolute, heading permalinks ("¶", "#" or an icon) are removed, and footnote markers such as \[1] or \[a] that only jump to a note on the same page are removed. Blog posts lose their tag, category and author link lines (and "By … " bylines with their avatars), and page-end "Related tags", "Related posts", share, follow, newsletter and comment-count sections; code samples lose their tab labels ("JavaScriptTypeScript", "npm Yarn pnpm"). A block repeated on many pages of the same site (a footer, a banner, a "this page is also available as…" note) is removed from the fourth page on; headings, tables, code, dates and file names above code always stay..

### Limitations

- Only public `http`/`https` pages; logins, private networks and local addresses are not supported. The one exception is **Custom request headers**, meant for your own docs or staging site (for example an `Authorization` token): they are sent only to the host(s) of your start URLs, never logged, and pages read with them are never rendered in a browser. This Actor does not log in to third-party sites and must not be used to access accounts or content you are not authorised to use.
- Sites that block automated access are reported as failed and not charged.
- With **Respect robots.txt** on, disallowed pages are skipped (counted in the SUMMARY record, not in the dataset).
- Linked documents up to 15 MB are converted; images are not described.
- `title` is the page's `<title>`; when a page has none (e.g. a bare HTML document), the first heading of the converted content is used instead, and it is `null` only if the page has no heading either.
- **Remove navigation, headers and footers** works from common HTML tags, roles and class names (`nav`, `footer`, `aside`, `[role=banner]`, `.site-header`, `.breadcrumb`, `.sidebar`…). Unusual layouts can keep some menu text; use **Also remove these elements** for those.

### Is it legal?

By default the crawler reads each site's robots.txt and skips the pages it disallows (they are free and counted as `disallowedByRobotsTxt` in the `SUMMARY` record); turn **Respect robots.txt** off only for sites you own or may crawl. It fetches only public pages: it does not log in or get past paywalls. Crawling publicly available pages is generally allowed, but you are responsible for how you use the content: respect the site's terms of service, copyright and privacy laws. This is not legal advice.

### FAQ

**My run shows failed pages (HTTP 5xx, 403 or "no text"). What now?** Failed pages are never charged. Each failed row has an `errorType` and a `hint` with the next step. HTTP 5xx means the site's own server errored: the Actor already read the page twice, so try again later. 403 or a bot check means the site refuses automated requests: set *Plain HTTP requests from* to Apify's network and add an Apify proxy (Advanced settings), or set *Page fetching* to *Browser*. A 404 means the address is wrong: start from the site's home page or documentation index. `routesTried` shows which routes (`backend`, `direct`, `vm`, `browser`) were attempted.

**What if my input is wrong (no URL, a typo, bad headers)?** The run ends normally without crawling or charging anything. The dataset has one row with `errorType: "invalid_input"` and a `hint`, and the `SUMMARY` record has `status: "INVALID_INPUT"`.

**Can I convert a list of URLs to Markdown without crawling?** Yes. Set **Crawl scope** to *Only the start URLs (no crawling)* and paste your URLs one per line.

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**How is it different from Apify's Website Content Crawler?** This Actor has a fixed price per page with compute included ($1 per 1,000 pages over plain HTTP, $2.50 with a browser). Website Content Crawler has more options and bills compute time, so a tuned run can cost less on large sites that work over plain HTTP. In our 5-site test with default settings, this Actor was faster on every site and kept more of the body text on docs and help-center pages, while WCC left less clutter on blog pages (see the comparison above).

**My start URL redirects to a new address (a docs site that moved). Is the crawl cut short?** No. After the start page is read, the folder or host it redirected to is added to the crawl scope (the original stays), so its links are followed; the redirect is listed in SUMMARY `scopeRedirected`. Include and exclude patterns still apply to the pages found.

**How do I go from a test crawl to the whole site?** Run 10-20 pages first, then open the run's `SUMMARY` record. If it has a `nextStep`, copy its `suggestedInput` (`crawlScope: "domain"` and a `maxPages` your budget covers) into a new run. `estimatedCostUsd` is the maximum, not a quote: you pay only for pages actually converted. See [From a test run to the whole site](#from-a-test-run-to-the-whole-site).

**Does it work on JavaScript websites?** Yes. In **Auto** mode pages that need JavaScript, or sites that block simple requests, are rendered in headless Chrome automatically and charged at the browser price.

**Is the output ready for RAG and vector databases?** Yes. Set **RAG chunk size (characters)** to get a `chunks` array split at paragraph boundaries with overlap, each with its `headingPath` and `charCount`. The LangChain and LlamaIndex examples above load the dataset directly.

**Can I crawl my own docs site that needs a token?** Yes, if it is your own site (or one you are authorised to access). Put the header in **Custom request headers (your own site only)**, e.g. `{"Authorization": "Bearer <token>"}`. It goes only to your start URLs' host over a direct connection (a redirect to another host drops it), values are stored encrypted and never logged, and such pages are read in fast mode without the browser fallback. Sitemaps and robots.txt are fetched without the headers, so pages that are only listed in a protected sitemap are not discovered; use links or list the URLs yourself.

**How much will a run cost before I start it?** At the start the log and status message show the estimate (Max pages x price per page, worst case and expected). You pay only for pages actually returned; failed, empty and duplicate pages are free.

**A page has several `<article>` blocks (a blog index, a feed). Is everything converted?** Yes. The converter narrows a page to one main element only when there is exactly one `<article>` (or one `role=main` / `<main>`); with several articles it keeps the whole page body, so none of them is cut off.

**What are the limits?** Up to 10,000 pages per run (**Max pages**; **Max pages per start URL** splits them across sites), public pages only, and linked documents up to 15 MB. The default 256 MB of memory covers roughly 5,000 pages; for 10,000 pages set 512 MB. The run never charges more than your maximum charge per run: it stops there and keeps the pages converted so far.

**How fast is it?** In our 5-site benchmark, each 25-page site took 3–34 seconds with default settings. Pages are processed in parallel (**Parallel pages**: 10 by default, up to 30); pages that need the browser take longer.

**Does it work with the API, schedules, webhooks, Make, n8n or Zapier?** Yes. Start runs from the Apify API, a schedule or a webhook, use the Apify apps for Make, n8n and Zapier, load the results into LangChain or LlamaIndex ([examples](#use-it-from-code)), or call it from AI agents through [MCP](#use-with-ai-agents-mcp).

### Support

Open an issue in the **Issues** tab with the URL and your input. Issues are checked regularly.

Found this useful? A short review on the Store helps other users find it.

### Suspected infinite URLs

Discovered URLs longer than 2,000 characters or with the same path segments repeated three times are skipped. After five consecutive non-2xx responses from URLs with the same parent path or query parameter names, new URLs in that group are skipped until a 2xx response resets the count. Skipped URLs are not requested or charged as crawl pages, and they do not use Max pages. `SUMMARY.skippedTrapUrls` reports the number skipped (0 when none). Explicit start URLs are still attempted.

# Actor input Schema

## `urls` (type: `array`):

Main input (fill this, or `startUrls`). Where to start crawling, one URL per line, e.g. the home page of a docs site. Bare domains (example.com) are fine; blank or invalid lines are skipped and listed in the results (not charged).

## `crawlScope` (type: `string`):

Which discovered links to follow. "Same folder" keeps the crawl under the start URL's path (best for docs); when the start page links to nothing in its own folder (docs sites that give every page its own folder, e.g. /en/getting-started/), the folder one level up is used for it, never the whole site. Values: path = Same folder as the start URL; domain = Whole website (incl. subdomains); page = Only the start URLs (no crawling).

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

Stop after this many pages in total (all start URLs together). You pay only for pages converted successfully.

## `maxPagesPerStartUrl` (type: `integer`):

With several start URLs: stop crawling one site after this many pages, so every site gets its share of Max pages. 0 = no per-site limit.

## `maxDepth` (type: `integer`):

How many clicks away from a start URL to go. 0 = only the start URLs.

## `goal` (type: `string`):

Optional. A few words on the information you need, e.g. "pricing plans and limits". Pages whose link text or URL match these words are crawled first, so a small Max pages still reaches them, and each page gets relevantBlocks: its best-matching sections. Word matching, no AI model; same price per page. Leave empty for a normal crawl.

## `useSitemap` (type: `string`):

Choose "off", "seed" (default) or "only", e.g. {"useSitemap":"seed"}. Off follows links only; seed queues sitemap.xml URLs alongside discovered links; only queues sitemap URLs without following links. Sitemap locations come from robots.txt or common paths. Ignored when crawl scope is "Only the start URLs". Content links are crawled before menu and sitemap URLs.

## `respectRobotsTxt` (type: `boolean`):

Skip pages the site's robots.txt disallows for crawlers. Skipped pages are not output, not charged and don't count toward Max pages (see disallowedByRobotsTxt in the SUMMARY record).

## `mode` (type: `string`):

"Auto" uses the cheap fast mode and switches to a real browser only when a page blocks it or needs JavaScript. "Fast" never uses a browser; "Browser" always does. Values: auto = Auto (recommended); fast = Fast (HTTP only); browser = Browser (JavaScript rendering).

## `cssSelector` (type: `string`):

Optional. Convert only this part of each page, e.g. "main" or "article", to drop menus and footers.

## `includeUrlPatterns` (type: `array`):

Optional glob patterns; only matching URLs are crawled. \* matches within a path segment, \*\* matches anything. Example: https://example.com/blog/\*\*

## `excludeUrlPatterns` (type: `array`):

Optional glob patterns to skip, e.g. **/tag/** or \**/login*

## `includeDocuments` (type: `boolean`):

Also convert linked PDF, Word, Excel and CSV files to Markdown.

## `outputFormats` (type: `array`):

Markdown keeps headings, links and tables. Plain text strips all formatting (good for NLP and word counts). Chunks are always made from the Markdown.

## `chunkSize` (type: `integer`):

If set (e.g. 2000), each page also gets a "chunks" array split at paragraph boundaries, ready for embeddings. 0 = off.

## `chunkOverlap` (type: `integer`):

Characters repeated between consecutive chunks to keep context.

## `combinedFile` (type: `boolean`):

Saves every converted page in crawl order as one Markdown file (llms-full.txt style), each page under a "# Title" header with a "Source:" line, as OUTPUT.md in the run's key-value store. Its link is in the SUMMARY record (combinedFileUrl). Very large crawls are split into 8 MB parts (OUTPUT-2.md, ...). Free: no extra charge. Set to false to skip it.

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

How many pages are processed at the same time.

## `httpVia` (type: `string`):

Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser. If both are refused, Auto also tries our second server (Oracle, different IP) before a real browser.

## `requestHeaders` (type: `object`):

Optional JSON object of HTTP header names and values for a site you own or may access, e.g. {"requestHeaders":{"Authorization":"Bearer <token>"}}. Headers go only to start-URL hosts over a direct connection and are dropped on cross-host redirects. Values are encrypted and not logged. Protected pages use fast mode without browser fallback; this does not log in to third-party sites.

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

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account.

## `removeBoilerplate` (type: `boolean`):

Recommended (on by default, also for API calls). Drops nav bars, site headers and footers, sidebars, breadcrumbs, cookie notices, "Skip to main content" links and table-of-contents widgets from the Markdown before it is saved. Links for crawling are still taken from the whole page. Set to false to keep the full page.

## `removeSelectors` (type: `string`):

Optional comma-separated CSS selectors removed from the Markdown, e.g. ".related-posts, #comments, .ad". Works with or without the option above.

## `tidyMarkdown` (type: `boolean`):

Removes leftover <!doctype> lines, empty headings, 1×1 tracking-pixel images, schema.org JSON-LD code blocks and exact repeated sections (e.g. a page rendered twice for desktop and mobile). Set to false to keep the converter output unchanged.

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

Same as the list above, in Apify's request-list format (also accepts a link to a text file of URLs). Kept for older inputs; the plain list above is recommended, especially for API calls.

## Actor input object example

```json
{
  "urls": [
    "https://docs.apify.com/academy"
  ],
  "crawlScope": "path",
  "maxPages": 10,
  "maxPagesPerStartUrl": 0,
  "maxDepth": 5,
  "useSitemap": "seed",
  "respectRobotsTxt": true,
  "mode": "auto",
  "includeDocuments": true,
  "outputFormats": [
    "markdown"
  ],
  "chunkSize": 0,
  "chunkOverlap": 200,
  "combinedFile": true,
  "maxConcurrency": 10,
  "httpVia": "auto",
  "requestHeaders": {},
  "removeBoilerplate": true,
  "tidyMarkdown": true
}
```

# Actor output Schema

## `pages` (type: `string`):

No description

## `full` (type: `string`):

No description

## `combined` (type: `string`):

No description

## `summary` (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 = {
    "urls": [
        "https://docs.apify.com/academy"
    ],
    "maxPages": 10,
    "requestHeaders": {},
    "removeBoilerplate": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/website-markdown-crawler").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 = {
    "urls": ["https://docs.apify.com/academy"],
    "maxPages": 10,
    "requestHeaders": {},
    "removeBoilerplate": True,
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/website-markdown-crawler").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 '{
  "urls": [
    "https://docs.apify.com/academy"
  ],
  "maxPages": 10,
  "requestHeaders": {},
  "removeBoilerplate": true
}' |
apify call tidytools/website-markdown-crawler --silent --output-dataset

```

## MCP server setup

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

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/TpZrf66Jvgs26hMby/builds/RQNYWcuPjN4AfqjRs/openapi.json
