Respuesta rápida
¿Cómo convierto una página web a Markdown en Node.js?
Usa el fetch integrado de Node.js 18+: llama a https://webreaderapi.com/api/v1/read?url=… con tu clave en el header Authorization y lee markdown de la respuesta JSON. WebReaderAPI devuelve el contenido principal de la página con su título, autor y fecha. Una página cuesta 1 crédito, 2 con navegador, desde 0,07 US$/1K páginas a octubre de 2026. Sin KYC; paga con criptomonedas.
¿Qué necesito?
Node.js 18 o posterior, para el fetch global, y una clave API en la variable de entorno WEBREADER_API_KEY. Los ejemplos son módulos ES: guárdalos con la extensión .mjs, o configura "type": "module" en tu package.json, para que funcione el await de nivel superior.
Los comentarios JSDoc describen la forma de la respuesta, así que editores como VS Code te dan autocompletado sin un paso de compilación de TypeScript.
/**
* @typedef {Object} Page
* @property {string} url
* @property {string} final_url
* @property {number} status_code
* @property {boolean} rendered
* @property {number} credits_used
* @property {string} [title]
* @property {string} [author]
* @property {string} [published_at]
* @property {string} [language]
* @property {string} [markdown]
* @property {number} word_count
*/
/** @returns {Promise<Page>} */
export async function readPage(url, options = {}) {
const params = new URLSearchParams({ url, ...options });
const res = await fetch(`https://webreaderapi.com/api/v1/read?${params}`, {
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
signal: AbortSignal.timeout(75_000),
});
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error.message), { code: body.error.code });
return body;
}
const page = await readPage('https://example.com/docs/getting-started', { max_chars: '20000' });
console.log(page.title, page.word_count, 'words,', page.credits_used, 'credit(s)');
¿Cómo leo una lista de URLs sin alcanzar el límite?
Ejecuta un número fijo de workers que toman URLs de una cola compartida. Es más simple que Promise.all sobre toda la lista, que iniciaría todas las solicitudes a la vez y alcanzaría 429 concurrency_limit. Cada cuenta tiene un límite de cuántas páginas lee a la vez; tu panel lo muestra y el soporte puede ampliarlo.
El pool de abajo mantiene los errores junto a los resultados, así que una página rechazada no detiene la ejecución. Los rechazos como blocked_by_robots son definitivos y gratuitos; cuéntalos y continúa.
import { readPage } from './read.mjs';
export async function readAll(urls, workers = 8) {
const queue = [...urls];
const results = [];
async function worker() {
while (queue.length) {
const url = queue.shift();
try {
const page = await readPage(url, { max_chars: '50000' });
results.push({ url, page });
} catch (err) {
results.push({ url, error: err.code ?? err.name });
}
}
}
await Promise.all(Array.from({ length: workers }, worker));
return results;
}
const urls = Array.from({ length: 30 }, (_, i) => `https://example.com/blog/post-${i + 1}`);
for (const r of await readAll(urls)) console.log(r.url, '->', r.error ?? r.page.title);
¿Cómo sigo un trabajo de rastreo en Node.js?
Inicia el trabajo con POST https://webreaderapi.com/api/v1/crawl y luego lee https://webreaderapi.com/api/v1/jobs/{job_id} en un bucle. Pasa cursor cada vez que next_cursor vuelva; cuando no haya cursor y el estado sea done, el trabajo ha terminado. Espera entre sondeos vacíos y duplica la espera hasta 30 segundos, ya que un rastreo que respeta el Crawl-delay de un sitio puede ser lento.
La página de batch y rastreo describe cómo el rastreo elige sus páginas y cómo limit limita lo que lee.
const headers = {
Authorization: `Bearer ${process.env.WEBREADER_API_KEY}`,
'Content-Type': 'application/json',
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const start = await fetch('https://webreaderapi.com/api/v1/crawl', {
method: 'POST',
headers,
body: JSON.stringify({ url: 'https://docs.example.com/', limit: 300, include_paths: ['/docs/*'] }),
});
const { job_id } = await start.json();
const pages = [];
let cursor = '';
let wait = 2000;
for (;;) {
const url = `https://webreaderapi.com/api/v1/jobs/${job_id}${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`;
const job = await (await fetch(url, { headers })).json();
pages.push(...job.results.filter((r) => !r.error));
if (job.next_cursor) {
cursor = job.next_cursor;
wait = 2000;
continue;
}
if (job.status === 'done') break;
await sleep(wait);
wait = Math.min(wait * 2, 30000);
}
console.log(pages.length, 'pages read, job', job_id);
¿Cómo paso el resultado a un modelo?
Envía el Markdown, no todo el JSON. El campo markdown es lo que un modelo debería leer; los metadatos es mejor guardarlos para las citas. Antepón el título y final_url, para que el modelo pueda nombrar su fuente, y mantén max_chars en un tamaño que el contexto de tu modelo pueda contener, como 20.000 caracteres por página.
La página de function calling de OpenAI muestra el bucle completo de herramientas en Python; la misma función readPage de arriba puede servir como cuerpo de la herramienta en un agente de Node.js.
Node.js: preguntas frecuentes
¿Funciona en Deno, Bun o el navegador?
Deno y Bun admiten el mismo fetch y los módulos ES. No llames a la API desde una página del navegador: la clave sería visible para todos los visitantes. Llámala desde tu servidor.
¿Hay un paquete npm?
No. Las funciones de esta página son toda la integración. WebReaderAPI es HTTPS y JSON sin más, así que mantienes el control de los reintentos, los timeouts y el logging.
¿Cómo manejo el 429 concurrency_limit?
Reduce el número de workers o reintenta tras una breve espera que se duplique cada vez. Un 429 no cuesta nada, y la página de errores tiene una función de backoff lista.
¿Cuánto cuesta leer 10.000 páginas?
Unos 10.000 créditos para páginas simples, más si algunas necesitan un navegador. 10.000 páginas cuestan 3,75 US$; Los créditos prepago nunca caducan: las páginas sin usar permanecen en tu cuenta hasta que las uses.