LLM Reader & Web2Markdown avatar

LLM Reader & Web2Markdown

Pricing

from $3.50 / 1,000 web page converteds

Go to Apify Store
LLM Reader & Web2Markdown

LLM Reader & Web2Markdown

Convert one public web page or JavaScript-rendered article into clean Markdown for LLM, RAG, and AI-agent pipelines using lightweight HTTP fetching with an SSRF-hardened Playwright fallback.

Pricing

from $3.50 / 1,000 web page converteds

Rating

0.0

(0)

Developer

Progamadores.com

Progamadores.com

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

Categories

Share

Convert one public web page into compact, main-content Markdown for LLMs, RAG ingestion, AI agents, and downstream content pipelines. This web page-to-Markdown API combines a lightweight HTTP fetch with an SSRF-hardened Playwright fallback for JavaScript-rendered pages.

The Actor uses Mozilla Readability to identify the primary article content, filters common page furniture, and uses Turndown to produce consistent Markdown plus structured source metadata and an approximate token count. It processes exactly one URL per run; it is an article content extractor and HTML-to-Markdown scraper, not a whole-site crawler.

Common use cases

  • Prepare a public article or documentation page for chunking, embeddings, and a RAG knowledge base.
  • Give an AI agent clean page text, title, author, source URL, and extraction metadata without passing raw HTML.
  • Normalize blog posts, help-center articles, or public reports before summarization or classification.
  • Extract readable content from a JavaScript-rendered page when the site's network behavior is compatible with the Actor's safety policy.
  • Feed a downstream content-monitoring or diff pipeline with repeatable Markdown snapshots, one URL at a time.

The Actor does not crawl links, discover sitemaps, process PDFs, or bypass authentication, CAPTCHAs, paywalls, access controls, or anti-bot protections.

Input

A minimal run only needs a public URL:

{
"url": "https://example.com/article"
}

All supported options:

{
"url": "https://example.com/article",
"render_js": false,
"browser_fallback": true,
"min_content_chars": 500,
"include_links": false,
"include_images": false,
"request_timeout_secs": 7,
"browser_timeout_secs": 20,
"overall_timeout_secs": 20,
"max_content_bytes": 3000000
}
FieldTypeRequiredDefaultAllowed values and behavior
urlstringYesAbsolute public http:// or https:// URL, at most 2,048 characters; URL fragments are discarded. URL credentials, local hostnames, private/link-local/loopback/special IP addresses, mixed public-private DNS answers, nonstandard ports, and unsafe redirects are rejected. Only ports 80 and 443 are supported.
render_jsbooleanNofalseWhen true, launch Playwright immediately. When false, try lightweight HTTP extraction first.
browser_fallbackbooleanNotrueWhen render_js is false, allow Playwright if lightweight extraction fails recoverably or is too short. Set to false for a strict fetch-only run.
min_content_charsintegerNo500From 100 to 10,000. Lightweight content below this text-character threshold triggers browser fallback when enabled. Content must also contain at least 20 words to be considered sufficient.
include_linksbooleanNofalseKeep filtered HTTP(S) and mailto: references. Relative links are resolved, but retained public-looking hostnames are not DNS-validated; validate them before any downstream fetch.
include_imagesbooleanNofalseKeep filtered image references in Markdown. The browser renderer never downloads image binaries; validate retained references before fetching them downstream.
request_timeout_secsintegerNo7From 2 to 25 seconds per HTTP request. The global extraction deadline can shorten this budget.
browser_timeout_secsintegerNo20From 5 to 60 seconds for Playwright navigation. The global extraction deadline always takes precedence.
overall_timeout_secsintegerNo20From 8 to 120 seconds, shared by HTTP, browser, and parsing phases. It starts after the Actor process begins, so it excludes container allocation and cold-start time.
max_content_bytesintegerNo3000000From 100,000 to 5,000,000 decoded HTML bytes for the main page. Browser subresources have separate per-request and total budgets.

Unknown input fields and values of the wrong type fail validation instead of being silently ignored.

Execution modes

ConfigurationBehaviorTypical reason to use it
render_js: false, browser_fallback: trueLightweight HTTP first; Playwright is used only after recoverable fetch/extraction failure or insufficient readable content.Default balance between compute use and compatibility.
render_js: false, browser_fallback: falseFetch and parse only; Chromium is never imported or launched.Strict cost and latency control for server-rendered HTML.
render_js: truePlaywright from the start; browser_fallback is irrelevant.Pages known to require client-side rendering.

Content is considered sufficient when it reaches both min_content_chars and 20 words. A forced-browser or fetch-only run can still succeed below that threshold and reports the condition in warnings. If a browser fallback fails after a usable lightweight candidate was extracted, the Actor returns that lightweight result with a warning.

The default application deadline is 20 seconds. The lightweight request defaults to 7 seconds and reserves time for a possible browser fallback. Container allocation happens before this application timer. Remote smoke tests observed fresh-container runs taking up to 33 seconds end to end, so the separate production gateway in this repository uses a 55-second Actor run timeout and a 60-second upstream deadline. These limited smoke observations are not an SLA, latency benchmark, or cost guarantee.

Runs that force or allow Playwright request 512 MB by default. Explicit browser_fallback: false fetch-only runs request 256 MB.

Output

On success, the Actor writes exactly one item to the default Dataset. The Dataset is the sole output contract; the Actor deliberately avoids a duplicate OUTPUT Key-Value Store copy.

The exact response shape from run-sync-get-dataset-items is a JSON array containing that one item. The values below are illustrative, while every field and nested field shown is part of the runtime contract:

[
{
"success": true,
"title": "Example article",
"author": "Ada Example",
"markdown": "## Main section\n\nClean content...",
"word_count": 1500,
"estimated_tokens": 2100,
"url": "https://example.com/article",
"final_url": "https://www.example.com/article",
"rendered_with": "fetch",
"extracted_at": "2026-08-30T12:00:00.000Z",
"warnings": [],
"metadata": {
"strategy": "lightweight",
"excerpt": "Clean content...",
"site_name": "Example",
"language": "en",
"published_time": null,
"content_characters": 9200,
"source_bytes": 48000,
"http_status": 200,
"token_estimation_method": "ceil(markdown_characters/4)",
"timing_ms": {
"total": 320,
"fetch": 240,
"browser": null,
"parse": 80
}
}
}
]

author, metadata.excerpt, metadata.site_name, metadata.language, and metadata.published_time can be null. rendered_with is either fetch or playwright; metadata.strategy is lightweight, browser-fallback, or browser-forced.

estimated_tokens is a model-independent heuristic (ceil(markdown.length / 4)), not an exact tokenizer result. Callers that require billing-grade counts should tokenize with the target model's tokenizer.

The Actor validates the serialized one-item Dataset response, including JSON escaping and the surrounding array, against the gateway's 1 MiB (1,048,576-byte) upstream limit. Markdown is capped at 960 KiB (983,040 UTF-8 bytes), leaving a documented 64 KiB allowance for URLs, metadata, JSON escaping, and the surrounding array. The complete serialized result is still checked, so an oversized result fails with CONTENT_TOO_LARGE instead of becoming a successful response that the gateway cannot consume.

API integration

Set APIFY_TOKEN to a token that is authorized to run the Actor. The synchronous endpoint returns an array, so each example explicitly reads its first item.

cURL

Requires jq to select and process the result:

curl --fail-with-body --request POST \
"https://api.apify.com/v2/actors/O0BUo5Aeddo3ealQ6/run-sync-get-dataset-items?clean=true&limit=1&timeout=55" \
--header "Authorization: Bearer ${APIFY_TOKEN}" \
--header "Content-Type: application/json" \
--data '{"url":"https://example.com/","render_js":false}' \
| jq -r '.[0] | "Title: \(.title)\nRenderer: \(.rendered_with)\n\n\(.markdown)"'

limit=1 is the response-item limit. The Actor itself writes exactly one item, so a pay-per-result maxItems query parameter is not needed to shape this response.

Python

Install the HTTP client with python -m pip install requests:

import os
import requests
response = requests.post(
"https://api.apify.com/v2/actors/O0BUo5Aeddo3ealQ6/run-sync-get-dataset-items",
params={"clean": "true", "limit": 1, "timeout": 55},
headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
json={"url": "https://example.com/", "render_js": False},
timeout=70,
)
response.raise_for_status()
items = response.json()
if not isinstance(items, list):
raise RuntimeError("Expected the Apify Dataset response to be a list")
if len(items) != 1:
raise RuntimeError(f"Expected one Dataset item, received {len(items)}")
result = items[0]
print(result["title"])
print(result["markdown"])

Node.js

This example uses the built-in fetch available in current Node.js versions:

const endpoint = new URL(
"https://api.apify.com/v2/actors/O0BUo5Aeddo3ealQ6/run-sync-get-dataset-items",
);
endpoint.search = new URLSearchParams({
clean: "true",
limit: "1",
timeout: "55",
});
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/",
render_js: false,
}),
signal: AbortSignal.timeout(70_000),
});
if (!response.ok) {
throw new Error(`Apify ${response.status}: ${await response.text()}`);
}
const items = await response.json();
if (!Array.isArray(items)) {
throw new Error("Expected the Apify Dataset response to be an array");
}
if (items.length !== 1) {
throw new Error(`Expected one Dataset item, received ${items.length}`);
}
const [result] = items;
console.log(result.title);
console.log(result.markdown);

For longer-running or disconnected workflows, start the Actor asynchronously with the standard Apify Run Actor endpoint, wait for completion or use a webhook, and then read defaultDatasetId. This Actor's output still remains the same single Dataset item.

Failure semantics

Input, security, HTTP, timeout, size, browser, and extraction failures fail the Actor run and do not push a misleading { "success": false } item. With run-sync-get-dataset-items, success is an HTTP response containing the one-item JSON array above; a failed run is a non-2xx Apify API response and must be handled before parsing Dataset items.

Normalized runtime error codes include INPUT_INVALID, SSRF_BLOCKED, REDIRECT_INVALID, DNS_LOOKUP_FAILED, HTTP_ERROR, REQUEST_FAILED, REQUEST_TIMEOUT, UNSUPPORTED_CONTENT_TYPE, EMPTY_CONTENT, CONTENT_TOO_LARGE, and BROWSER_FAILED. Persisted logs and terminal errors contain normalized codes and error types, never raw target URLs or upstream/Playwright messages.

Warnings are successful-result diagnostics, not failures. They can report validated redirects, blocked browser resources, a browser fallback, short readable content, or a failed browser fallback followed by a usable lightweight result.

Browser safety and resource limits

Playwright is imported and Chromium is launched only in a forced or automatic fallback path. Chromium never receives direct network access for page HTTP traffic: every document, redirect, script, stylesheet, XHR, and fetch request is intercepted, DNS-validated, IP-pinned, downloaded through the safe Node client, and fulfilled back into the page. Service workers and WebSockets are blocked. Non-idempotent browser methods, including POST, as well as popups, downloads, images, media, fonts, event streams, and response cookies are blocked.

Those controls intentionally limit compatibility. Sites that require POST-based GraphQL or data calls, response-cookie sessions, authentication, or similar state may not hydrate even with render_js: true. The option means JavaScript execution is forced; it does not relax the network safety policy.

The browser path allows at most 120 HTTP requests, 8 simultaneous downloads, 2 MB per subresource, max_content_bytes for a document, and at most 15 MB total or three times the configured main-page limit, whichever is lower. The lightweight path applies max_content_bytes cumulatively across every redirect response and the final response, rather than granting a fresh allowance per hop. The HTML default is 3 MB and is configurable up to 5 MB.

A linear preflight rejects source markup with more than 50,000 approximate opening elements, and Playwright checks the live DOM before serializing it. These limits deliberately trade some compatibility for bounded memory and SSRF resistance. JSDOM and Readability parsing are synchronous and cannot be interrupted mid-operation; byte and element caps plus deadline checkpoints bound the practical risk but do not make parsing fully preemptible.

When links or images are enabled, their protocol and obvious URL shape are filtered, but public-looking hostnames inside extracted content are not DNS-resolved. Treat retained references as untrusted data and validate them before any downstream fetch.

Responsible use

Use the Actor only for pages you are authorized to access and process. Respect site terms, copyright, privacy, rate limits, and applicable law. robots.txt is a crawler convention and can be context-dependent; this single-page Actor does not automatically fetch or interpret it. Check it yourself when your use case requires it.

Local development

Node.js 24.15 or newer is required.

npm ci
npm run check
npm run build

For a local Actor run, place valid JSON at storage/key_value_stores/default/INPUT.json and run npx apify-cli run. Networked runs should target only a page you control or are allowed to scrape. Unit tests use inline HTML and injected DNS resolvers; they do not access the network.

The production Docker image pins the official apify/actor-node-playwright-chrome:24-1.62.1 base in both stages and pins the matching Playwright npm package to 1.62.1. Forced-browser and automatic-fallback paths were smoke-tested remotely at 512 MB before making it the default allocation. These tests verify the execution paths, not universal site compatibility.