Website Content Crawler for LLMs & RAG
Eimantas V · @amethyst
Crawl any website to clean Markdown for RAG and LLMs: navigation and footers stripped, tokens counted, ready-to-embed chunks. No browser, no proxy needed.

About
Website Content Crawler for LLMs & RAG
Point it at a site. Get clean Markdown back, with the navigation and footer gone, the token count already worked out, and — if you want them — chunks ready to embed.
No browser. No API key. No proxy for most sites.
docs.apify.com/academy 24 pages 5.1s 1.3 MB 15 nav blocks stripped 180×
en.wikipedia.org 9 pages 2.1s 2.2 MB
react.dev/learn 15 pages 3.0s 3.1 MB
About 0.2 seconds a page. Every one of those sites serves its content over plain HTTP, so that is how this reads them.
The three things it does that a page-at-a-time extractor cannot
1. It learns the site's furniture from the crawl
A single page cannot tell its navigation from its article. Both are text in tags, and rules about position or tag name are wrong often enough to ruin an index.
A crawl can. The nav, the footer, the cookie bar and the "on this page" sidebar are exactly the blocks that appear on nearly every page — and the article is the part that does not. So the filter is learned from your corpus rather than guessed per page.
Two signals, because one is not enough:
- Frequency. A block on most pages of the crawl is furniture. Counted once per page, so a nav of forty links on one page cannot out-vote the same nav appearing on forty pages.
- Link density. Frequency alone missed the sidebars on docs.apify.com: documentation renders a different sidebar per section, so each nav sat on only three or four pages of twelve — under any sane threshold, and all of them survived into the content. A block that is almost entirely link text with no prose between the links is a menu whatever its frequency. Ordinary writing does not look like that; a paragraph with three citations still has sentences around them.
Blocks are matched with the link targets stripped, keeping only the link text. A nav highlights the page you are on, which makes it textually unique on every page it appears on — with the hrefs left in, a twelve-page crawl saw twelve different navs and removed none of them.
2. It counts tokens
Every page and every chunk carries tokenCount. RAG budgets are in tokens, not bytes, and the number you need at chunking time is the one nobody gives you.
It is an estimate, not a tokenizer call — shipping tiktoken would add megabytes of vocabulary to an actor whose whole argument is that it is cheap. Within roughly 10% on mixed prose, code and markup, and it deliberately errs high: a chunk smaller than budgeted is a non-event, one larger is a rejected embedding call. CJK is counted near one token per character rather than by the chars-over-four rule, which under-counts a Chinese page threefold.
3. It admits when a page needs a browser
Some pages really are client-rendered. Those come back with needsJavaScript: true instead of an empty body, so you can send those few URLs to a browser rather than discovering blank documents in your index three weeks later.
It does not fire on merely short pages. Across a crawl of linear.app — a Next.js site — it fired zero times, because linear.app server-renders.
What a page looks like
{
"url": "https://docs.apify.com/api",
"title": "Apify API documentation",
"content": "# Apify API documentation\n\nLearn how to use the [Apify platform](https://docs.apify.com/) programmatically.\n\n## REST API\n\nThe Apify API is built around HTTP REST...\n\n```bash\ncurl https://api.apify.com/v2/acts\n```",
"wordCount": 357,
"tokenCount": 943,
"headings": ["Apify API documentation", "REST API", "OpenAPI schema", "API clients"],
"needsJavaScript": false,
"boilerplateBlocksRemoved": 17,
"fetchedInMs": 184
}
Headings, lists, tables and fenced code survive with their language hints. Links stay as [text](url) so the model can cite and you can follow.
And a chunk
{
"chunkId": "a3f9c21b0e44-0002",
"url": "https://docs.apify.com/api",
"chunkIndex": 2,
"chunkCount": 6,
"heading": "API clients",
"content": "## API clients\n\nThe client libraries are a more convenient way...",
"tokenCount": 780,
"overlapTokens": 96
}
chunkId is a hash of the URL and the index, so a re-crawl updates rows instead of duplicating them. Chunks break on block boundaries — never mid-sentence, and never through the middle of a code fence. heading carries forward, so a chunk taken from halfway down a page still says what section it belongs to.
What it will not do
- It does not run JavaScript. That is the point — it is why it costs what it costs. Pages that need a browser are flagged, not rendered.
- It does not pretend a 403 is a rate limit. Some sites refuse datacenter IPs; you get
HTTP_403immediately rather than three retries and twenty wasted seconds. Add a proxy for those. - It does not normalise dates.
publishedAtis whatever the page's meta tag said. Guessing a timezone the page never stated would invent precision. - It respects robots.txt by default, including
Crawl-delay, longest-rule-winsAllowoverDisallow, and*/$wildcards. An emptyDisallow:means permission, not a ban on everything.
Scope
The crawl stays on the hosts you started from. includeSubdomains widens it to the same registrable domain — and that is computed properly: taking the last two labels of bbc.co.uk would give co.uk, and a crawl scoped to that would accept every .co.uk address on the internet.
URLs are normalised before they are queued: fragment dropped, default port dropped, query sorted, tracking parameters removed. Left alone, a crawl spends its whole budget re-reading one article under a hundred different utm strings.
Input
{
"startUrls": ["https://docs.apify.com/academy"],
"maxPages": 200,
"maxDepth": 3,
"chunk": true,
"chunkTokens": 800,
"chunkOverlapTokens": 100
}
Everything else has a sensible default. useSitemap is on, which is the cheapest way to find every page on a site: one request, and it follows a sitemap index to the nested sitemaps it points at.
Pricing
One flat monthly rental, shown on this page, with a free trial. There is no per-page fee: crawl as many pages as you need.
Runs use your credits for compute and any proxy traffic, as every Actor does. The crawler fetches pages over plain HTTP with no proxy by default, so compute is the whole cost. Measured on Bytewells: 49 documentation pages in 11 seconds at 512 MB for $0.0004 of credits: about $0.01 per 1,000 pages.
Pages below your minimum word count are skipped. Pages robots.txt refused, URLs that errored, and responses that were not readable pages come back as error rows with an errorCode instead of content. A client-rendered page is delivered, flagged needsJavaScript: true — telling you it needs a browser is the finding you came for.
FAQ
- What does it return for each page?
- The page's main content as Markdown, with its title, headings, word count and token count, and a flag that says whether the page needs JavaScript. Optional chunks come ready to embed, each with its heading, token count and overlap. Chunk IDs are stable, so a re-crawl updates rows instead of duplicating them.
- How does it remove navigation and footers?
- It learns them from the crawl itself: blocks that appear on nearly every page (menus, footers, cookie bars, sidebars) are furniture, and the article is what differs. Link density is used as a second signal. Headings, lists, tables, code blocks and links are kept.
- Does it run JavaScript?
- No, which is why it is fast and cheap. Pages that build their text in the browser are not rendered; they are delivered with needsJavaScript set to true instead of coming back as a blank page.
- How much does it cost?
- One flat monthly rental, shown on this page, with a free trial and no per-page fee. Pages are fetched over plain HTTP with no proxy by default, so compute is the whole cost. Measured on Bytewells: 49 documentation pages in 11 seconds for $0.0004 of credits, about $0.01 per 1,000 pages.
- Which pages does it crawl?
- Only the hosts you start from, or the whole registrable domain with Include subdomains. URLs are normalised before they are queued (fragments and tracking parameters removed), so one article is not read under many addresses. robots.txt is respected by default, including Crawl-delay.
- Why did a site return HTTP_403?
- Some sites refuse datacenter addresses. The crawler reports HTTP_403 at once instead of retrying; add a proxy in the input for those sites.
- Can I use it through the API or from AI agents?
- Yes. Start runs through the Bytewells API (bytewells.com/docs/api), schedule them and read the dataset as JSON; webhooks work too. The API is Apify-compatible, so apify-client works with the Bytewells base URL. AI agents can run it as a tool through the Bytewells MCP server (bytewells.com/docs/mcp).
Input
Preview of the input form. Rent the Actor to fill it in and start runs from the console.
Fields
startUrlsWhere to begin. A bare domain works — example.com becomes https://example.com. Everything crawled stays on these hosts unless you turn on Include subdomains.
maxPagesHow many pages to read at most. This is the number you are charged for, and the crawl stops the moment it is reached.
maxDepthHow many links deep to follow from a start URL. 0 reads only the start URLs themselves. Pages found in the sitemap enter at depth 1.
followLinksDiscover more pages by following links found on each page. Turn this off to read only the start URLs and whatever the sitemap lists.
useSitemapRead sitemap.xml first, including any sitemaps robots.txt points at and any nested sitemap index. It is the fastest way to find every page on a site and costs one request.
respectRobotsTxtHonour the site's crawl rules and its Crawl-delay. Turn this off only for sites you own or have permission to crawl.
includeSubdomainsAlso crawl other hosts on the same root domain, so a start URL on example.com will also read docs.example.com and blog.example.com.
includeUrlPatternsOnly crawl URLs matching one of these. Globs by default (/blog/), or write /regex/ for a regular expression. Leave empty to allow everything in scope.
excludeUrlPatternsSkip URLs matching any of these. Same glob or /regex/ syntax. Useful for /tag/, /author/ and other pages that hold no content.
outputFormatmarkdown keeps headings, lists, tables and code fences, which is what an LLM reads best. text is the same content with the markup stripped. html returns the original source untouched.
removeBoilerplateDrop the navigation, footer, cookie bar and sidebar. Blocks repeating across the crawl are learned and removed, and link-only menus are removed on sight even when the crawl is too small to learn from.
keepLinksKeep links as Markdown text. Turn off for prose only — smaller, but the model can no longer cite or follow anything.
keepImagesInclude images as . Off by default: alt text is rarely worth the tokens unless you are indexing figures.
keepTablesRender tables as Markdown rows. Off means the cells still appear, but the row structure is lost.
minWordsSkip pages with fewer words than this once boilerplate is gone — redirect stubs, empty tag pages, bare listings. Skipped pages are not charged. A page that needs JavaScript is always kept, because its emptiness is the finding.
chunkAlso emit ready-to-embed chunks as their own records, each with a stable id, its token count and the heading it sits under.
chunkTokensTarget size per chunk. Chunks break on block boundaries, so a chunk never starts mid-sentence and a code fence is never cut in half.
chunkOverlapTokensTokens carried from the end of one chunk into the start of the next, so a fact spanning a boundary survives. Must be below the chunk size.
flattenOutputTurn arrays into pipe-delimited strings so the CSV and Excel exports have a fixed, readable column set.
maxConcurrencyPages fetched at once. Raise it for large crawls of sites that can take it; lower it if you get rate limited.
requestDelayMsMilliseconds to wait between requests. Raised automatically if robots.txt asks for a longer Crawl-delay.
requestTimeoutSecsSeconds to wait for one page before giving up on it.
maxRetriesRetries for a page that times out or answers with a retryable status.
proxyConfigurationOptional. Not needed for most sites — this actor sends plain HTTP requests and does not run a browser.
API
Bytewells speaks the Apify API. Existing integrations keep working: point apify-client at https://bytewells.com/api and use your Bytewells API token (Console → Settings → API access). The Actor ID is amethyst/llm-content-crawler (or amethyst~llm-content-crawler in URLs).
JavaScript · apify-client
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({
token: process.env.BYTEWELLS_TOKEN,
baseUrl: 'https://bytewells.com/api',
});
const input = {
"startUrls": [
"https://docs.apify.com/academy"
],
"maxPages": 50,
"maxDepth": 3,
"followLinks": true,
"useSitemap": true,
"respectRobotsTxt": true,
"includeSubdomains": false,
"outputFormat": "markdown",
"removeBoilerplate": true,
"keepLinks": true,
"keepImages": false,
"keepTables": true,
"minWords": 25,
"chunk": false,
"chunkTokens": 800,
"chunkOverlapTokens": 100,
"flattenOutput": false,
"maxConcurrency": 5,
"requestDelayMs": 200,
"requestTimeoutSecs": 45,
"maxRetries": 2
};
const run = await client.actor('amethyst/llm-content-crawler').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);Python · apify-client
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["BYTEWELLS_TOKEN"], api_url="https://bytewells.com/api")
run_input = {
"startUrls": [
"https://docs.apify.com/academy"
],
"maxPages": 50,
"maxDepth": 3,
"followLinks": True,
"useSitemap": True,
"respectRobotsTxt": True,
"includeSubdomains": False,
"outputFormat": "markdown",
"removeBoilerplate": True,
"keepLinks": True,
"keepImages": False,
"keepTables": True,
"minWords": 25,
"chunk": False,
"chunkTokens": 800,
"chunkOverlapTokens": 100,
"flattenOutput": False,
"maxConcurrency": 5,
"requestDelayMs": 200,
"requestTimeoutSecs": 45,
"maxRetries": 2
}
run = client.actor("amethyst/llm-content-crawler").call(run_input=run_input)
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(items)HTTP · curl
curl -X POST "https://bytewells.com/api/v2/acts/amethyst~llm-content-crawler/runs" \
-H "Authorization: Bearer $BYTEWELLS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"startUrls":["https://docs.apify.com/academy"],"maxPages":50,"maxDepth":3,"followLinks":true,"useSitemap":true,"respectRobotsTxt":true,"includeSubdomains":false,"outputFormat":"markdown","removeBoilerplate":true,"keepLinks":true,"keepImages":false,"keepTables":true,"minWords":25,"chunk":false,"chunkTokens":800,"chunkOverlapTokens":100,"flattenOutput":false,"maxConcurrency":5,"requestDelayMs":200,"requestTimeoutSecs":45,"maxRetries":2}'Runs are billed as compute from your credit balance. Paid Actors need an active rental or trial; otherwise the API answers 402 rental-required.
Related
More Actors from the same publisher.

RAG Web Browser - Google Search to Markdown for AI
Eimantas V · @amethyst
Web search and fetch for AI agents and RAG: searches Google and returns the top results as clean Markdown with token counts, or reads any URL. No browser.
AIDeveloper toolsSEO tools$19/mo7-day free trial
Google Search Results Scraper - SERP API
Eimantas V · @amethyst
Scrape Google search results by keyword: organic results with ranks, places, top stories, knowledge panel, related searches, News and Videos. Any country.
SEO toolsMarketingLead generation$19/mo7-day free trial
Facebook Ads Library Scraper - Meta Ads, All Countries
Eimantas V · @amethyst
Scrape the Facebook Ads Library in any country: ad copy, images, videos, landing pages, CTAs, run dates and advertiser Pages. No login, no token needed.
MarketingSocial mediaBusiness$29/mo7-day free trial
Facebook Pages Scraper - Emails, Phones & Followers
Eimantas V · @amethyst
Scrape Facebook Pages by link, username or ID: email, phone, website, address, exact followers, category, rating and social links. No login or cookies.
Lead generationSocial mediaMarketing$19/mo7-day free trial
Facebook Posts Scraper - Pages, Profiles & Reactions
Eimantas V · @amethyst
Scrape posts from public Facebook Pages and profiles: text, date, reactions by type, comments, shares, video views and media. Date filters and new-post alerts.
Social mediaMarketingLead generation$19/mo7-day free trial
Google Maps Scraper - Business Leads & Emails
Eimantas V · @amethyst
Scrape Google Maps places by keyword and city: name, address, phone, website, rating, reviews, hours and coordinates, plus emails and socials from websites.
Lead generationTravelBusiness$19/mo7-day free trial