Skip to content
Search/
CyberMax › Answers

How to convert a whole website to Markdown for RAG, with llms.txt

Updated 2026-11-10 · Data and API builders · by CyberMax

Short answer

Crawl the site from its sitemap and links, keep only each page's main content (no menus, footers or cookie banners) and convert it to Markdown with code fences and tables intact, then split it on headings into chunks your embedding model can take. Docsieve on Apify does all of that and also writes llms.txt and llms-full.txt for the site, at $0.001 per page; the llms.txt files are free and scheduled runs charge only for changed pages.

  1. Give the start page of the site or docs (https://docs.example.com). The sitemap and internal links are followed; robots.txt is respected.
  2. Limit the crawl with includePaths ("/docs/*") and excludePaths ("/blog/tag/*") and set maxPages.
  3. Choose outputMode "chunks" for one row per chunk (ready to embed) or "pages" for one Markdown document per page.
  4. Upsert the chunks by chunkId into your vector database (Pinecone, Qdrant, Weaviate, pgvector, Chroma).
  5. Schedule the same input with monitor mode on: later runs return only new and changed pages, plus the URLs removed since the last run.

Copy-paste code

Python (pip install apify-client): docs site to RAG chunks

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("cybermax/website-to-markdown").call(run_input={
    "startUrls": ["https://docs.apify.com/academy"],
    "includePaths": ["/academy*"],
    "maxPages": 200,
    "outputMode": "chunks",      # one row per chunk
    "chunkSize": 2000, "chunkOverlap": 200,
})
for c in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(c["chunkId"], c["heading"], c["chunkTokens"])  # embed c["text"], keep c["url"] as the source

kv = client.key_value_store(run["defaultKeyValueStoreId"])
open("llms.txt", "w").write(kv.get_record("LLMS-TXT")["value"])

LangChain (Python): load the run straight into Documents

from langchain_community.document_loaders import ApifyDatasetLoader
from langchain_core.documents import Document

loader = ApifyDatasetLoader(
    dataset_id=run["defaultDatasetId"],
    dataset_mapping_function=lambda d: Document(
        page_content=d["text"], metadata={"source": d["url"], "heading": d["heading"]}),
)
docs = loader.load()  # the chunks are already heading-aware: embed them as they are

JavaScript / Node (npm i apify-client): pages as Markdown files

import { ApifyClient } from 'apify-client';
import { writeFileSync } from 'node:fs';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('cybermax/website-to-markdown').call({
  startUrls: ['https://docs.example.com'], maxPages: 300,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((p, i) => writeFileSync(`page-${i}.md`, `<!-- ${p.url} -->\n${p.markdown}`));

No code: n8n, Make or Zapier

n8n: HTTP Request node (or the Apify node, action "Run an Actor and get dataset")
Method:  POST
URL:     https://api.apify.com/v2/acts/cybermax~website-to-markdown/run-sync-get-dataset-items
Auth:    Header Auth  ->  Authorization: Bearer <your Apify token>
Body:    JSON  {"startUrls": ["https://docs.yourproduct.com"], "monitor": true, "outputMode": "chunks"}
Output:  one n8n item per result row

Make:    Apify app -> "Run an Actor" (Actor: cybermax/website-to-markdown, wait for finish = yes)
         -> Apify "Get Dataset Items" (dataset = the run's defaultDatasetId) -> your next module
Zapier:  Apify app -> action "Run Actor" (cybermax/website-to-markdown, input JSON as above)
         -> trigger "Finished Actor Run" -> "Fetch Dataset Items" -> your app
Runs longer than 5 minutes: start with POST /v2/acts/cybermax~website-to-markdown/runs and read the dataset when the run finishes.

Fresh RAG index: Schedule trigger (daily 03:00) -> the request above -> your vector store node (Pinecone, Qdrant, Supabase) "Upsert" by chunkId -> delete chunks whose url is in the OUTPUT record's removedSinceLastRun.

From an AI agent (MCP)

# Any MCP client (Claude Desktop, Claude Code, Cursor, VS Code): Apify's hosted MCP server
{
  "mcpServers": {
    "website-to-markdown": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.apify.com/?tools=cybermax/website-to-markdown",
               "--header", "Authorization: Bearer ${APIFY_TOKEN}"],
      "env": { "APIFY_TOKEN": "<your Apify token>" }
    }
  }
}
# The agent sees one tool, cybermax--website-to-markdown, with the Actor's input schema.
# Ask: "read docs.example.com as Markdown and summarise how authentication works".
Real run on Apify (10 Nov 2026: Apify Academy, includePaths /academy*, 10 pages in 5.2 s; four rows shown)
PageWordsTokens (est.)Chunks
/academy2544891
/academy/advanced-web-scraping/crawling/sitemaps-vs-search1,0001,5814
/academy/ai/ai-agents1,3212,5406
/academy/anti-scraping2,5514,27111

Source: Docsieve (Apify Actor cybermax/website-to-markdown) README, real Apify run on 10 Nov 2026

Run Docsieve on Apify

Why clean Markdown matters for RAG

Raw HTML wastes tokens on navigation, footers and scripts, and naive text extraction loses code blocks and tables. Main-content Markdown keeps the structure the model needs, and heading-aware chunks (with a heading path such as Guide > Install > Linux) retrieve far better than fixed-size slices of a page.

Generate llms.txt and llms-full.txt for your site

Every run writes LLMS-TXT (site name, summary and a link list grouped by section) and LLMS-FULL-TXT (every page's Markdown with its source URL) to the run's storage, free. Publish them at your site root so AI assistants and AI search can read your site cleanly.

Give an AI agent a whole site to read

Through Apify's MCP server an agent can call Docsieve to read a docs site as Markdown before answering, alongside the other CyberMax web data tools (company domains, contacts, tech stack, trends).

Good to know

Price on Apify: $0.001 per page converted ($1 per 1,000), no start fee; a 500-page docs site is $0.50. llms.txt files are free, and thin, noindex and robots-blocked pages are skipped and not charged. Apify's free plan includes $5 of usage each month.

Docsieve reads the HTML the server sends and does not render JavaScript; most docs platforms (Docusaurus, GitBook, MkDocs, Mintlify, WordPress, server-rendered Next.js) work as is.

Docsieve: Turn any website or docs site into clean Markdown, RAG chunks and llms.txt

$0.001 per page, no start fee

Docsieve turns a website or docs site into clean Markdown, RAG-ready chunks and llms.txt, and re-runs only changed pages. $0.001 per page; llms.txt is free.

Runs on your own Apify account, pay per result; failed results are not charged.

Questions

How are tokens counted?

Estimated at 4 characters per token, close to GPT and Claude tokenizers for English text.

How do I keep a chatbot's index fresh?

Schedule the run with monitor mode on. Only new and changed pages come back (and are charged), and the run summary lists removed URLs so you can delete their chunks.

Is it legal to convert any site?

Docsieve reads public pages and obeys robots.txt. Convert sites you own or have the right to use, and check each site's terms for how you may use its content.

Can an AI agent use it?

Yes. Apify's MCP server exposes it as cybermax--website-to-markdown for Claude, Cursor and other MCP clients.

Free: the Free API Starter Kit

Copy-paste calls for 4 APIs with free daily tiers (CVE priority, VIN recalls, US duty rates, email checks), then 5 short emails on KEV/EPSS, MCP and free data.

You get one email to confirm first. Then the kit, and 5 short emails over two weeks. One-click unsubscribe in every email; we never share your address.

Related answers

How to give Claude, Cursor or any AI agent web data tools through Apify's MCP serverAdd Apify's MCP server to Claude, Cursor or VS Code in one config block: company domains, contacts, tech stackHow to run an SEO audit and broken link check on a whole siteCrawl a whole website and get every page's SEO issues, broken links, duplicate titles and a 0-100 score, worst