Docs
Dashboard
Examples / Data outBeta

Site to Markdown

Crawl a site into clean Markdown files, one per page, with each page's address and title at the top: ready for AI search. The crawler respects robots.txt and crawl-delay.

When to use it: You want a site's pages as Markdown files, for example to feed AI search or an index.

Usescrawl.start and crawl.wait (breadth-first, same host)
Needsany plan with crawls; no model
Sitequotes.toscrape.com (a demo site)
Outputoutput/site/<page>.md and output/site/index.json
LanguagesNode and Python

The code

The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.

/** * Site to Markdown: crawl a site into clean Markdown files, one per page, for AI search (robots.txt is respected). * *   npx tsx node/index.ts            (BOXLINE_API_KEY; START_URL and MAX_PAGES for your own site) * * Writes output/site/<page>.md (with the page's address and title at the top) and output/site/index.json. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const start = process.env.START_URL ?? "https://quotes.toscrape.com/";const maxPages = Number(process.env.MAX_PAGES ?? 6);const bx = new Boxline();// Breadth-first from the start page, same host only, a few pages at a time; robots.txt and crawl-delay are respected.const job = await bx.crawl.start({ url: start, maxPages, maxDepth: 2, format: "markdown" });console.log(`Crawl ${job.id} started`);const done = await bx.crawl.wait(job.id, { pollMs: 1500, timeoutMs: 10 * 60_000 });console.log(`${done.status}: ${done.pagesDone} pages, ${done.pagesFailed} failed, ${done.skippedByRobots} skipped by robots.txt`);const slug = (u: string) => {  const { pathname, search } = new URL(u);  return (pathname + search).replace(/^\/|\/$/g, "").replace(/[^a-zA-Z0-9]+/g, "-").slice(0, 80) || "index";};const dir = join(out, "site");mkdirSync(dir, { recursive: true });const index: { url: string; title: string; file: string; chars: number }[] = [];for (const page of done.data) {  if (page.error || !page.content || (page.status ?? 0) >= 400) continue;  const file = `${slug(page.finalUrl ?? page.url)}.md`;  writeFileSync(join(dir, file), `---\nurl: ${page.finalUrl ?? page.url}\ntitle: ${JSON.stringify(page.title ?? "")}\n---\n\n${page.content}\n`);  index.push({ url: page.finalUrl ?? page.url, title: page.title ?? "", file, chars: page.content.length });  console.log(`  ${file}  ${page.content.length} chars  ${page.title}`);}writeFileSync(join(dir, "index.json"), JSON.stringify(index, null, 2));writeFileSync(join(out, "result.json"), JSON.stringify({ start, crawlId: job.id, status: done.status, pagesDone: done.pagesDone, skippedByRobots: done.skippedByRobots, files: index }, null, 2));

Inputs

Environment variables the example reads:

  • START_URL: where the crawl starts (default https://quotes.toscrape.com/).
  • MAX_PAGES: how many pages at most (default 6).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/site-to-markdown. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.

# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=…   # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts

Then check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.

What its check proves

check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.

At least 3 Markdown files, all from the start page's host, each with its header and no HTML left; the start page holds its first quote.

Source

github.com/Boxline-dev/examples/site-to-markdown on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.