> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Scrape a page, then screenshot its links

vision returns the demo board's links; render captures each linked page in a styled browser frame. One key, two products, no glue service.

One scrape gives you the page as Markdown and every link on it. Pass the links you want to render and you get a styled
screenshot of each, framed and on a background, ready for a changelog, a docs page or a review. Both calls take the
same key; nothing runs between them but your loop.

Verified 2026-11-01: this recipe's last run passed against `https://beta-api.b3os.org`, label `live-recorded` (recorded from production), from commit `771a9508`. The recipe harness runs it again, and a failed run takes this page out of the cookbook.

## Run it

**curl**

```bash
START=https://beta.b3os.org/demo/
curl -s https://beta-api.b3os.org/v1/vision/scrape \
  -H "Authorization: Bearer $B3OS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://beta.b3os.org/demo/", "formats": ["markdown", "links"]}' \
  | jq -r --arg s "$START" '.links[] | select(startswith($s) and . != $s)' | head -3 > pages.txt

while read -r url; do
  jq -n --arg url "$url" '{url: $url, viewport: {width: 1440, height: 900},
    style: {background: "midnight", frame: "browser-dark"}, format: "webp"}' \
  | curl -s https://beta-api.b3os.org/v1/render/screenshots \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" -d @- \
  | jq -r '.artifacts[0].url'
done < pages.txt
```
**TypeScript**

```ts
const API = "https://beta-api.b3os.org";
const START = "https://beta.b3os.org/demo/";
const headers = { authorization: `Bearer ${process.env.B3OS_API_KEY}`, "content-type": "application/json" };
const post = async (path: string, body: unknown): Promise<any> =>
  (await fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) })).json();

const page = await post("/v1/vision/scrape", { url: START, formats: ["markdown", "links"] });
const pages = (page.links as string[]).filter((u) => u.startsWith(START) && u !== START).slice(0, 3);
for (const url of pages) {
  const shot = await post("/v1/render/screenshots", {
    url,
    viewport: { width: 1440, height: 900 },
    style: { background: "midnight", frame: "browser-dark" },
    format: "webp",
  });
  console.log(url, shot.artifacts[0].url);
}
```
**Python**

```python
import os
import requests

API = "https://beta-api.b3os.org"
START = "https://beta.b3os.org/demo/"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"

page = s.post(f"{API}/v1/vision/scrape", json={"url": START, "formats": ["markdown", "links"]}).json()
pages = [u for u in page["links"] if u.startswith(START) and u != START][:3]
for url in pages:
    shot = s.post(f"{API}/v1/render/screenshots", json={
        "url": url,
        "viewport": {"width": 1440, "height": 900},
        "style": {"background": "midnight", "frame": "browser-dark"},
        "format": "webp",
    }).json()
    print(url, shot["artifacts"][0]["url"])
```
**MCP**

With the [B3OS MCP server](/sdks/mcp) connected, ask your agent:

```text
Scrape https://beta.b3os.org/demo/ for its links, then take a screenshot of up to three demo pages it links to,
with the midnight background and the browser-dark frame.
```

The agent calls `vision_scrape` once and `render_screenshot` once per page.

The recipe's `run.mjs` is the same flow with checks. Each screenshot URL is signed and stops working at its
`url_expires_at`; see [render limits](/render/limits) for what else a screenshot is held to.

Owner: Docs. Products: Docs, Vision, Render.

## Output of the last green run

**Command**

```bash
$ node run.mjs
# exit 0
```
**Outputs**

```json
{
  "scrape_status": 200,
  "markdown_chars": 1038,
  "demo_links": 4,
  "pages": [
    "/demo/billing/",
    "/demo/pricing/",
    "/demo/slow/"
  ],
  "screenshots": 3,
  "all_succeeded": true,
  "content_type": "image/webp",
  "smallest_bytes": 29368,
  "scrape_ms": 893.8,
  "slowest_screenshot_ms": 2163.7,
  "images": [
    "https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/96a5f2a383c16f06.webp",
    "https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/397948ca0889a48d.webp",
    "https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/78780b5e381d0b38.webp"
  ]
}
```

![Image 1 of 3 from the last green run of the recipe Scrape a page, then screenshot its links](https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/96a5f2a383c16f06.webp)
![Image 2 of 3 from the last green run of the recipe Scrape a page, then screenshot its links](https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/397948ca0889a48d.webp)
![Image 3 of 3 from the last green run of the recipe Scrape a page, then screenshot its links](https://beta.b3os.org/gallery/recipes/scrape-to-screenshots/78780b5e381d0b38.webp)

The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-01 from commit `771a9508`, label `live-recorded` (recorded from production). Case `recipes/scrape-to-screenshots`.