# Chuyển trang web sang Markdown trong Node.js

> Chuyển trang web sang Markdown trong Node.js bằng fetch: pool concurrency nhỏ và vòng polling job. Từ 0,07 US$/1K trang, không cần KYC, trả bằng crypto.

- URL: https://webreaderapi.com/vi/docs/node/
- Cập nhật lần cuối: 2026-10-22
- Đơn vị phát hành: WebReaderAPI (https://webreaderapi.com)

Node.js 18 trở lên đã có sẵn `fetch`, nên bạn không cần cài thêm dependency nào. Ba công thức: đọc một trang, đọc một danh sách với pool concurrency nhỏ, và theo dõi một crawl job cho đến khi xong.

**Câu trả lời nhanh — Làm sao để chuyển một trang web sang Markdown trong Node.js?**

Dùng `fetch` có sẵn của Node.js 18+: gọi `https://webreaderapi.com/api/v1/read?url=…` với khóa API của bạn trong header `Authorization` và đọc `markdown` từ câu trả lời JSON. **WebReaderAPI** trả về nội dung chính của trang kèm tiêu đề, tác giả và ngày. Một trang tốn 1 credit, 2 credit nếu dùng trình duyệt, từ 0,07 US$/1K trang tính đến tháng 10 năm 2026. Không cần KYC; thanh toán bằng crypto.

## Tôi cần gì?

Node.js 18 trở lên, để có `fetch` toàn cục, và một khóa API trong biến môi trường `WEBREADER_API_KEY`. Các ví dụ là ES module: lưu chúng với đuôi `.mjs`, hoặc đặt `"type": "module"` trong `package.json` của bạn, để `await` ở cấp cao nhất hoạt động được.

Các chú thích JSDoc mô tả cấu trúc của response, nên các editor như VS Code sẽ gợi ý hoàn thành mà không cần đến bước build TypeScript.

*read.mjs: một trang*

```javascript
/**
 * @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)');

```

## Làm sao để đọc một danh sách URL mà không chạm rate limit?

Chạy một số lượng worker cố định lấy URL từ một hàng đợi chung. Cách này đơn giản hơn `Promise.all` trên toàn bộ danh sách, vì làm vậy sẽ khởi chạy mọi request cùng lúc và chạm `429 concurrency_limit`. Mỗi tài khoản có giới hạn số trang đọc cùng lúc; dashboard của bạn hiển thị giới hạn đó, và bộ phận hỗ trợ có thể tăng lên.

Pool bên dưới giữ lỗi ngay cạnh kết quả, nên một trang bị từ chối không làm dừng cả lượt chạy. Những lần bị từ chối như `blocked_by_robots` là chung thẩm và miễn phí; hãy đếm chúng rồi đi tiếp.

*pool.mjs: một danh sách URL, 8 cái một lúc*

```javascript
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);

```

## Làm sao để theo dõi một crawl job trong Node.js?

Khởi động job bằng `POST https://webreaderapi.com/api/v1/crawl`, rồi đọc `https://webreaderapi.com/api/v1/jobs/{job_id}` trong một vòng lặp. Truyền `cursor` mỗi lần `next_cursor` trả về; khi không còn cursor và trạng thái là `done`, job đã xong. Hãy chờ giữa các lần poll rỗng và tăng gấp đôi thời gian chờ tối đa 30 giây, vì một crawl tôn trọng Crawl-delay của trang có thể chậm.

Trang [batch và crawl](https://webreaderapi.com/vi/docs/crawl/) mô tả cách crawl chọn trang và cách `limit` giới hạn những gì nó đọc.

*crawl.mjs: khởi động một crawl và thu thập mọi trang*

```javascript
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);

```

## Làm sao để truyền kết quả cho model?

Gửi Markdown, không phải toàn bộ JSON. Trường `markdown` là thứ model nên đọc; phần metadata thì nên giữ để trích dẫn nguồn. Thêm tiêu đề và `final_url` vào đầu, để model có thể nêu nguồn, và giữ `max_chars` ở kích thước mà context của model chứa được, ví dụ 20.000 ký tự mỗi trang.

Trang [OpenAI function-calling](https://webreaderapi.com/vi/docs/openai/) trình bày đầy đủ vòng lặp tool bằng Python; chính hàm `readPage` ở trên cũng có thể làm thân tool trong một agent Node.js.

## Node.js: các câu hỏi thường gặp

### Cái này có chạy trong Deno, Bun hay trình duyệt không?

Deno và Bun hỗ trợ cùng `fetch` và ES module. Đừng gọi API từ trang trình duyệt: khóa API sẽ lộ cho mọi người truy cập. Hãy gọi từ server của bạn.

### Có package npm không?

Không. Các hàm trên trang này là toàn bộ phần tích hợp. WebReaderAPI chỉ là HTTPS và JSON thuần, nên bạn giữ được quyền kiểm soát retry, timeout và logging.

### Làm sao để xử lý lỗi 429 concurrency_limit?

Giảm số worker, hoặc retry sau một khoảng chờ ngắn tăng gấp đôi mỗi lần. Lỗi 429 không tốn gì, và [trang errors](https://webreaderapi.com/vi/docs/errors/) có sẵn một hàm backoff.

### Đọc 10.000 trang tốn bao nhiêu?

Khoảng 10.000 credit cho các trang thường, nhiều hơn nếu một số trang cần dùng trình duyệt. 10.000 trang tốn 3,75 US$; Credit trả trước không bao giờ hết hạn: số trang chưa dùng vẫn ở trong tài khoản của bạn cho đến khi bạn dùng.

---

WebReaderAPI: WebReaderAPI là web reader API cho AI agent, chuyển mọi trang web công khai thành Markdown sạch, thanh toán bằng crypto không cần tải lên giấy tờ. Credit trả trước từ 0,07 US$/1K trang không bao giờ hết hạn, và robots.txt được tôn trọng. Không cần KYC — không ID, không selfie, không giấy tờ. Thanh toán bằng crypto.
