+
+
+
+
+
+`client.scrape()` takes a URL and returns the page already converted to Markdown. That matters because Markdown is the format large language models read best: headings, lists, and links survive, while the script tags, tracking pixels, and nav chrome that bloat a raw HTML dump are gone. You get a string you can drop straight into a prompt, with no headless Chrome on your machine and no DOM parsing in your code.
+
+```typescript
+const scraped = await client.scrape({
+ url: TARGET_URL,
+ format: ["markdown"],
+});
+
+const markdown = scraped.content.markdown ?? "";
+```
+
+`scrape()` runs the fetch and the cleanup on Steel's side, so there is no session to create, connect to, or release. One HTTP call in, structured content out. The same `client.screenshot()` and `client.pdf()` calls render the same page two other ways.
+
+## Markdown for model context
+
+The reason to reach for `scrape()` over a browser library is the format. A raw page is mostly markup a model has to wade through: a single news article can be tens of thousands of tokens of `` soup before the first sentence. Markdown collapses that to the text, the structure, and the links, so you spend tokens on content instead of tags. The wiring is small once you have the string:
+
+```typescript
+const { content, metadata } = await client.scrape({
+ url: TARGET_URL,
+ format: ["markdown"],
+});
+
+const answer = await llm.chat({
+ messages: [
+ { role: "system", content: "Answer using only the page below." },
+ { role: "user", content: `# ${metadata.title}\n\n${content.markdown}` },
+ ],
+});
+```
+
+That is the whole integration: scrape to Markdown, prepend the title, hand it to a model. No selectors, no `page.evaluate`, no waiting on a DOM you do not control.
+
+One failure mode to plan for: a heavily client-rendered page can return near-empty Markdown if the content paints after the initial load. When `content.markdown` comes back short for a site you know is rich, add `delay` (milliseconds) to the `scrape()` call so the page settles before capture. Check `metadata.statusCode` too. A scrape of a 403 or a soft-blocked page still succeeds at the HTTP level but hands you the block page's text, not the content you wanted.
+
+## What you get back
+
+`format` is an array, so you can ask for more than one representation in a single call: `["markdown", "html", "cleaned_html", "readability"]`. Each lands under `content` on the response (`content.markdown`, `content.html`, and so on), and the field is undefined when you did not request that format, which is why the example reads `content.markdown ?? ""`.
+
+The response carries more than the body. `scraped.metadata` holds the page `title`, `description`, `statusCode`, Open Graph tags, and the canonical URL. `scraped.links` is a flat array of `{ text, url }` for every link on the page, handy when you want an LLM to pick a next page to visit. The example prints the status code, title, link count, and the first 500 characters of Markdown so you can see the shape without dumping a whole article to the terminal.
+
+`screenshot()` and `pdf()` differ from `scrape()` in one way worth knowing up front: they return a hosted URL, not bytes. `shot.url` and `pdf.url` point at the rendered artifact on Steel's storage, so the example logs the links rather than writing files. If you want the bytes on disk, fetch the URL yourself. The Python sibling does exactly that.
+
+## Run it
+
+```bash
+cd examples/scrape-ts
+cp .env.example .env # set STEEL_API_KEY
+npm install
+npm start
+```
+
+Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). `TARGET_URL` in `.env` is optional and defaults to Hacker News.
+
+Your output varies. Structure looks like this:
+
+```text
+Steel Scrape API (TypeScript)
+============================================================
+
+Scraping https://news.ycombinator.com to markdown...
+HTTP 200 | Hacker News
+Links found: 174
+Markdown length: 6841 characters
+
+--- Markdown preview (first 500 chars) ---
+# Hacker News
+
+* [new](newest)
+* [past](front)
+* [comments](newcomments)
+* [ask](ask)
+* [show](show)
+...
+--- end preview ---
+
+Capturing a full-page screenshot...
+Screenshot hosted at: https://steel-screenshots.s3.amazonaws.com/...
+
+Rendering the page to PDF...
+PDF hosted at: https://steel-screenshots.s3.amazonaws.com/...
+
+Done. Feed the markdown straight into an LLM prompt.
+```
+
+Each of the three calls is one billed request against Steel, so a full run costs a few cents of browser time. There is no session left open to leak: `scrape()`, `screenshot()`, and `pdf()` each return when the work is finished, so unlike the browser-driving recipes there is no `release()` to forget.
+
+## Make it yours
+
+- **Pipe Markdown into a model.** Pass `markdown` as the user message to your LLM of choice and ask it to summarize the page or pull out structured fields. This is the whole reason to scrape to Markdown instead of HTML.
+- **Ask for several formats at once.** Set `format: ["markdown", "html"]` when you want the clean text for the model and the raw HTML for a fallback parser, both from a single request.
+- **Bundle artifacts into the scrape.** Instead of separate `screenshot()` and `pdf()` calls, pass `screenshot: true` and `pdf: true` to `scrape()`. The URLs come back on `scraped.screenshot` and `scraped.pdf`, which is one billed request instead of three.
+- **Get past anti-bot pages.** Add `useProxy: true` to route through Steel's residential proxies, or `delay: 3000` to wait for client-side rendering before the capture.
+- **Pick a region.** `region` accepts values like `"iad"` or `"fra"` to run the fetch closer to the target or to your users.
+
+## Related
+
+[Python version](/cookbook/scrape) renders the same endpoints and writes the screenshot and PDF to disk as files. [Rust version](/cookbook/scrape) is the lowest-friction way into the Rust SDK. For a recipe that drives a real browser instead of the direct API, see [playwright-ts](/cookbook/playwright). Full method and parameter reference lives in the [steel-sdk package](https://www.npmjs.com/package/steel-sdk).
+
+
+
+
+
+
+
+
+
+Steel's `/v1/scrape` endpoint runs a browser server-side and hands back the rendered page. There is no session to create, no CDP socket to attach to, and no browser library on your machine. You call one method, and you get the page content, plus an optional screenshot and PDF. This recipe turns that single call into three files on disk: `page.md`, `screenshot.png`, and `page.pdf`.
+
+```python
+result = client.scrape(
+ url=TARGET_URL,
+ format=["markdown"],
+ screenshot=True,
+ pdf=True,
+)
+```
+
+The one detail worth internalizing: the response mixes inline data and hosted artifacts. `result.content.markdown` is a string you can write straight to a file. But `result.screenshot.url` and `result.pdf.url` are **hosted URLs**, not bytes. Steel renders the image and PDF, stores them, and returns links. So the recipe writes the markdown directly, then fetches the two URLs with `urllib` and saves the bytes. The `download` helper does the fetch; `main` wires the three writes.
+
+Because there is no session object, there is no teardown. `client.sessions.release(...)` does not apply here. You pay for the render, the response comes back, and you are done. That makes scrape the lowest-friction way to pull a page into an agent's context: one call, structured output, no lifecycle to manage.
+
+## Run it
+
+```bash
+cd examples/scrape-py
+cp .env.example .env # set STEEL_API_KEY
+uv run main.py
+```
+
+Grab a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). `uv sync` runs automatically on first `uv run`, so there is no separate install step.
+
+Your output varies. Structure looks like this:
+
+```text
+Steel Scrape API (Python)
+============================================================
+Scraping https://news.ycombinator.com ...
+Fetched "Hacker News" (HTTP 200)
+Markdown: 8421 chars, 147 links
+Saved page.md (8421 chars)
+Saved screenshot.png (184320 bytes)
+Saved page.pdf (96774 bytes)
+
+Artifacts written to /path/to/examples/scrape-py/output
+Done!
+```
+
+The three files land in `output/` next to `main.py`. Open `page.md` to see the markdown an LLM would read, `screenshot.png` for the rendered viewport, and `page.pdf` for a print-layout capture.
+
+A scrape costs a few cents of browser time. You are billed per render, not per minute, so a one-shot scrape is cheaper than spinning up a full session for the same page. If you only need text, drop `screenshot=True` and `pdf=True` and you skip the render-and-host work for the artifacts you are not using.
+
+## Make it yours
+
+- **Change the target.** Set `TARGET_URL` in `.env`, or edit the default in `main.py`. Everything downstream is the same.
+- **Pick your formats.** `format` accepts any of `markdown`, `html`, `cleaned_html`, and `readability`. Pass a list to get several at once, then read them off `result.content` (`result.content.html`, `result.content.cleaned_html`, and so on). `cleaned_html` strips scripts and boilerplate; `readability` returns article-extracted structure.
+- **Mine the metadata.** `result.metadata` carries `title`, `description`, `status_code`, Open Graph fields (`og_title`, `og_image`), `canonical`, `author`, and `json_ld`. `result.links` is a list of `{text, url}` for every link on the page, which is a ready-made frontier for a crawler.
+- **Get the artifacts without the markdown.** `client.screenshot(url=..., full_page=True)` and `client.pdf(url=...)` are standalone calls that each return a single hosted URL. Use them when you want a capture and nothing else. `full_page=True` captures past the fold.
+- **Reach difficult sites.** Pass `use_proxy=True` to route the render through Steel's residential proxy network for pages that block datacenter traffic.
+
+## How scrape differs from a browser session
+
+The other recipes in the cookbook connect a browser library (Playwright, Selenium) to a live Steel session over CDP, then drive clicks and reads themselves. That is the right tool when you need to log in, fill forms, or step through an app. Scrape is the right tool when you just want the page as it renders: one request in, content out, nothing to keep alive. If your agent's job is "read this URL," reach for scrape first and graduate to a session only when you need interaction.
+
+## Related
+
+[TypeScript version](/cookbook/scrape) covers the same endpoint with the clean-markdown-for-LLM angle. [Rust version](/cookbook/scrape) walks the three calls separately. For a live, interactive browser instead, see [playwright-py](/cookbook/playwright).
+
+
+
+
+
+
+
+
+
+Steel's REST API turns a URL into structured content without a browser on your side. The `steel-rs` crate wraps three of those endpoints as plain async methods: `client.scrape()` returns parsed content plus typed metadata, `client.screenshot()` and `client.pdf()` render the page and hand back a hosted file URL. There is no session to create, connect to, or release. Each call is one stateless request that runs a browser on Steel's side and returns when the page is done.
+
+That makes this the shortest path into Steel from Rust, and it leans on the SDK's typed structs rather than raw JSON. `scrape()` deserializes into a `ScrapeResponse`, so the fields are real Rust types you can pattern-match on:
+
+```rust
+let scraped = client
+ .scrape(ClientScrapeParams {
+ url: TARGET_URL.to_string(),
+ format: Some(vec![ScrapeRequestFormatItem::Markdown]),
+ // remaining options set to None; see main.rs
+ })
+ .await?;
+
+let meta = &scraped.metadata; // ScrapeResponseMetadata
+meta.status_code; // i64
+meta.title.as_deref(); // Option<&str>
+meta.language.as_deref(); // Option<&str>
+scraped.links.len(); // Vec
+scraped.content.markdown; // Option
+```
+
+`metadata` carries about twenty parsed fields (Open Graph tags, canonical URL, author, published time, the HTTP status code), so you get the document's shape without writing a single selector. `content` holds whichever formats you asked for in `format`: `Markdown`, `HTML`, `CleanedHTML`, or `Readability`. Request only what you need; markdown alone keeps the payload small for LLM context.
+
+`main` runs all three calls against Hacker News, prints the typed metadata, and writes `page.md`, `screenshot.png`, and `page.pdf` to the working directory. Screenshot and PDF responses are a hosted URL, not bytes, so the `download` helper fetches each URL with `reqwest` and writes the file. The artifacts live on Steel for a while after the call, which is handy if you would rather hand the URL to another service than store the bytes yourself.
+
+## Run it
+
+```bash
+cd examples/scrape-rs
+cp .env.example .env # set STEEL_API_KEY
+cargo run
+```
+
+Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The first build pulls `steel-rs`, `tokio`, and `reqwest`, so it takes a moment; later runs are fast.
+
+Your output varies. Structure looks like this:
+
+```text
+Scraping https://news.ycombinator.com ...
+ status 200
+ title Hacker News
+ language en
+ links 183
+ markdown 14217 chars
+ wrote page.md
+Capturing screenshot ...
+ wrote screenshot.png
+Rendering PDF ...
+ wrote page.pdf
+Done.
+```
+
+Three calls cost a few cents of browser time total. Steel bills per session-minute, and these one-shot endpoints spin up and tear down their own browser, so there is nothing to leak: no cleanup call, no session left running against the default 5-minute timeout. The trade-off is that each call is independent, so you cannot log in once and scrape five pages behind the auth. For that, open a session and drive a real browser (see Related).
+
+## Make it yours
+
+- **Change the target.** Edit the `TARGET_URL` constant. Every call reads from it.
+- **Pick formats.** Pass more variants in `format`, for example `vec![ScrapeRequestFormatItem::Markdown, ScrapeRequestFormatItem::HTML]`, then read `scraped.content.html`. Each requested format comes back as its own `Option` field on `content`.
+- **Get the screenshot and PDF in one call.** `scrape()` takes `pdf: Some(true)` and `screenshot: Some(true)`; the URLs come back on `scraped.pdf` and `scraped.screenshot` instead of making three round trips.
+- **Handle anti-bot pages.** Set `use_proxy: Some(true)` on any of the params to route through a Steel residential proxy. Add `delay: Some(2000)` to wait for late-loading content before capture.
+- **Match on the status.** `meta.status_code` is an `i64`, so branch on it before trusting the content (a soft 404 still returns markdown).
+
+## Related
+
+[TypeScript version](/cookbook/scrape) and [Python version](/cookbook/scrape) cover the same three endpoints. For a full browser session you connect to and drive over CDP, see [chromiumoxide](/cookbook/chromiumoxide). For the HTTP surface these methods wrap, see the [reqwest docs](https://docs.rs/reqwest) and [Tokio docs](https://tokio.rs).
+
+
+
+
+
+
+
+
+
+Steel's direct API turns a URL into clean content with no browser library and no session to manage. One `client.Scrape` call runs a browser server-side and returns the page as Markdown (or HTML, readability, or cleaned HTML) inline, while `client.Screenshot` and `client.Pdf` render the same page to hosted files. This recipe scrapes a page to Markdown, prints a preview, then captures a full-page screenshot and a PDF. It is the lowest-friction way to reach a page from Go: no CDP, no chromedp, no `defer release`.
+
+The scrape call leads:
+
+```go
+scraped, err := client.Scrape(ctx, steel.ClientScrapeParams{
+ URL: targetURL,
+ Format: &[]steel.ScrapeRequestFormatItem{steel.ScrapeRequestFormatItemMarkdown},
+})
+markdown := deref(scraped.Content.Markdown, "")
+title := deref(scraped.Metadata.Title, "(no title)")
+```
+
+Two Go specifics show up here. Optional request fields are pointers (`Format` is a `*[]ScrapeRequestFormatItem`, `FullPage` is a `*bool`), and steel-go ships no pointer constructors, so the recipe defines a one-line `ptr[T]` generic. Response fields like `Content.Markdown` and `Metadata.Title` are `*string`, so a small `deref` helper supplies a fallback. The format is a typed constant (`steel.ScrapeRequestFormatItemMarkdown`), not a bare string.
+
+Screenshot and PDF come back as hosted URLs, not bytes:
+
+```go
+shot, _ := client.Screenshot(ctx, steel.ClientScreenshotParams{URL: targetURL, FullPage: ptr(true)})
+fmt.Println(shot.URL) // https://...
+
+pdf, _ := client.Pdf(ctx, steel.ClientPdfParams{URL: targetURL})
+fmt.Println(pdf.URL)
+```
+
+To keep the files, fetch each URL with `net/http` and write the bytes to disk.
+
+## Run it
+
+```bash
+cd examples/scrape-go
+cp .env.example .env # set STEEL_API_KEY
+go run .
+```
+
+Get a Steel key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). Point it at any page with `TARGET_URL` in `.env`. Your output varies. Structure looks like this:
+
+```text
+Steel Scrape API (Go)
+============================================================
+
+Scraping https://news.ycombinator.com to markdown...
+HTTP 200 | Hacker News
+Links found: 184
+Markdown length: 8423 characters
+
+--- Markdown preview (first 500 chars) ---
+[ clean Markdown for the page ]
+--- end preview ---
+
+Capturing a full-page screenshot...
+Screenshot hosted at: https://...
+Rendering the page to PDF...
+PDF hosted at: https://...
+
+Done. Feed the markdown straight into an LLM prompt.
+```
+
+A scrape call costs a few cents of browser time. Steel starts and tears down the browser per call, so there is no session to release.
+
+## Make it yours
+
+- **Change the page.** Set `TARGET_URL` in `.env`, or pass a different URL to `client.Scrape`.
+- **Ask for several formats.** `Format` takes a slice, so request more than one at once (`ScrapeRequestFormatItemMarkdown`, `...HTML`, `...Readability`, `...CleanedHTML`). Each lands under its own field on `Content`.
+- **Save the artifacts.** Fetch `shot.URL` and `pdf.URL` with `net/http` and `os.WriteFile` to write `screenshot.png` and `page.pdf`, the way the Python recipe does.
+- **Scrape behind a proxy.** Set `UseProxy: ptr(true)` to route through a Steel residential proxy for geofenced or bot-sensitive pages.
+
+## Related
+
+[scrape-ts](/cookbook/scrape) and [scrape-py](/cookbook/scrape) are the same direct API in TypeScript and Python, where the Python recipe writes the screenshot and PDF to disk. [scrape-rs](/cookbook/scrape) is the Rust version. For a full browser you drive yourself, [chromedp](/cookbook/chromedp) and [Rod](/cookbook/rod) connect over CDP instead.
+
+
+
+
+
+## Related recipes
+
+
+
+
+
+
diff --git a/content/docs/cookbook/selenium.mdx b/content/docs/cookbook/selenium.mdx
index b2fba364..4ab5bc15 100644
--- a/content/docs/cookbook/selenium.mdx
+++ b/content/docs/cookbook/selenium.mdx
@@ -3,9 +3,9 @@ title: Automate a cloud browser with Selenium
description: Use Steel with Selenium in Python for cloud browser automation.
---
-
+
-
+
@@ -99,7 +99,7 @@ A run costs a few cents of session time. Steel bills per session-minute, so `mai
## Related recipes
-
-
-
+
+
+
diff --git a/content/docs/cookbook/stagehand.mdx b/content/docs/cookbook/stagehand.mdx
index 32287ea1..7c67a81e 100644
--- a/content/docs/cookbook/stagehand.mdx
+++ b/content/docs/cookbook/stagehand.mdx
@@ -3,13 +3,13 @@ title: Automate browsing with natural-language instructions using Stagehand
description: Use Steel with Stagehand for natural-language-driven AI browser automation.
---
-
+
-
+
@@ -105,7 +105,7 @@ A full run takes ~30 seconds and costs a few cents of Steel session time plus Op
-
+
@@ -250,7 +250,7 @@ A full run takes ~30 seconds. The `finally` block in `main()` calls `stagehand.s
## Related recipes
-
-
-
+
+
+
diff --git a/content/docs/cookbook/swiftide.mdx b/content/docs/cookbook/swiftide.mdx
new file mode 100644
index 00000000..c4c45c30
--- /dev/null
+++ b/content/docs/cookbook/swiftide.mdx
@@ -0,0 +1,108 @@
+---
+title: Build a research agent with Swiftide
+description: "Use Steel with Swiftide to build an agent whose tool reads the web through Steel's scrape endpoint, so the model works from clean Markdown with no browser library."
+---
+
+
+
+
+
+
+
+[Swiftide](https://swiftide.rs) is a Rust framework for LLM applications: indexing pipelines, query pipelines, and agents that loop over tool calls until they reach an answer. This recipe builds an agent whose only tool reads the web through Steel's `scrape` endpoint, so the model works from clean Markdown instead of raw HTML and never touches a browser library or CDP.
+
+The agent runs on Anthropic (`claude-sonnet-4-6`) and the tool is a `#[derive(Tool)]` struct that owns the Steel client:
+
+```rust
+#[derive(Clone, swiftide::Tool)]
+#[tool(
+ description = "Fetch a web page through a Steel cloud browser and return it as clean \
+ Markdown along with the page's outbound links. Use this to read a URL.",
+ param(name = "url", description = "Absolute URL of the page to read, including https://")
+)]
+struct ReadPage {
+ client: Arc,
+}
+
+impl ReadPage {
+ async fn read_page(&self, _ctx: &dyn AgentContext, url: &str) -> Result {
+ let response = self.client.scrape(ClientScrapeParams {
+ url: url.to_string(),
+ format: Some(vec![ScrapeRequestFormatItem::Markdown]),
+ ..
+ }).await?;
+ // ... return response.content.markdown plus response.links
+ }
+}
+```
+
+The derive macro reads the struct's snake-case name (`ReadPage` -> `read_page`), finds the method with that name, and turns each `#[tool(param(...))]` into a JSON Schema field via `schemars`. Anything that implements `Tool` slots into `Agent::builder().tools(...)`, so a stateful struct and a `#[swiftide::tool]` free function are interchangeable at the call site. The struct form is what lets the tool hold `Arc`; a free function has nowhere to put it.
+
+Wiring the agent is four builder calls:
+
+```rust
+let anthropic = Anthropic::builder().default_prompt_model("claude-sonnet-4-6").build()?;
+
+let mut agent = Agent::builder()
+ .llm(&anthropic)
+ .tools(vec![ReadPage { client: Arc::clone(&client) }])
+ .system_prompt(SYSTEM_PROMPT)
+ .limit(8)
+ .build()?;
+
+agent.query(TASK).await?;
+```
+
+`query` drives the loop: Claude reads the task, calls `read_page` on Hacker News, optionally follows one or two links the scrape returned, then calls the always-present `stop` tool when it has the answer. `.limit(8)` caps the round trips so a confused model can't loop forever. The `on_new_message` hook in `main` prints each assistant turn as it lands.
+
+## Run it
+
+```bash
+cd examples/swiftide
+cp .env.example .env # set STEEL_API_KEY and ANTHROPIC_API_KEY
+cargo run
+```
+
+Get a Steel key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys) and an Anthropic key at [console.anthropic.com](https://console.anthropic.com/settings/keys). The Anthropic client reads `ANTHROPIC_API_KEY` from the environment on its own; the Steel key is passed to `Steel::new` explicitly.
+
+Your output varies. Structure looks like this:
+
+```text
+Steel + Swiftide research agent
+============================================================
+ read_page: https://news.ycombinator.com (18243 chars)
+The highest-scoring story on the front page is "Show HN: ..." with 642
+points, submitted by pg. Let me open it to summarize.
+ read_page: https://news.ycombinator.com/item?id=43218921 (9117 chars)
+Top story: "Show HN: ..." by pg, 642 points. It is a ... . The author
+built it to ... and the thread debates ... .
+
+Done. Steel scrape calls bill a little browser time; no session to release.
+```
+
+Each `scrape` call spins up a short-lived Steel browser server-side, so a run costs a few cents of browser time plus a few thousand Anthropic tokens. There is no long-lived session to release here: `scrape` opens and closes its own browser per call, which is the trade for not managing a session yourself. If you switch to `client.sessions().create(...)` for a persistent browser, you own the `release` call and Steel bills per session-minute until you make it.
+
+## One thing that will bite you
+
+**The `#[derive(Tool)]` macro needs `serde` and `async-trait` as direct dependencies.** The expansion emits a bare `#[async_trait::async_trait]` and a `serde`-derived args struct without a `#[serde(crate = ...)]` override, so both crates have to resolve at the crate root even though you never name them. They are in `Cargo.toml` for that reason alone. The `#[swiftide::tool]` attribute macro on a free function fully qualifies its paths and does not need them, so that is the lighter option when your tool is stateless.
+
+Steel's request builders implement `IntoFuture` with a `Send` future, so `client.scrape(...).await` works directly inside a Swiftide tool even though tools run on a multi-threaded Tokio runtime.
+
+## Make it yours
+
+- **Swap the task.** Change `TASK` and `SYSTEM_PROMPT` in `main.rs`. The tool stays the same; the agent re-plans against the new goal.
+- **Give it more reach.** The tool already returns up to 40 of the page's links, which is what lets the model follow a story into its comments. Raise `.limit(8)` if you want it to crawl deeper, and widen or drop the link cap.
+- **Add a second tool.** A `screenshot` tool backed by `client.screenshot(...)` (returns a base64 PNG) or a `pdf` tool backed by `client.pdf(...)` drops in as another `#[derive(Tool)]` struct in the `tools(vec![...])` list. The agent picks per turn.
+- **Change the model.** Any Anthropic chat model works in `default_prompt_model`. Swiftide also ships OpenAI, Gemini, Groq, and Ollama integrations behind feature flags; swap the `Anthropic` builder for one of those and the tools are unaffected.
+
+## Related
+
+[Steel + rig (Rust)](/cookbook/rig) drives a real browser over CDP with chromiumoxide instead of the `scrape` endpoint. [Swiftide agent docs](https://swiftide.rs/agents/overview/) cover hooks, the `Tool` trait, and multi-agent setups.
+
+## Related recipes
+
+
+
+
+
+
diff --git a/content/docs/cookbook/topics/agents.mdx b/content/docs/cookbook/topics/agents.mdx
index 58f04cf8..7e01b02f 100644
--- a/content/docs/cookbook/topics/agents.mdx
+++ b/content/docs/cookbook/topics/agents.mdx
@@ -4,23 +4,30 @@ description: Agent frameworks that run a perception-plan-act loop against a Stee
---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/docs/cookbook/topics/authentication.mdx b/content/docs/cookbook/topics/authentication.mdx
index 94690372..f752ace2 100644
--- a/content/docs/cookbook/topics/authentication.mdx
+++ b/content/docs/cookbook/topics/authentication.mdx
@@ -4,7 +4,7 @@ description: Patterns for persisting and replaying authenticated sessions across
---
-
-
-
+
+
+
diff --git a/content/docs/cookbook/topics/browser-automation.mdx b/content/docs/cookbook/topics/browser-automation.mdx
index cca555d4..0f44cf7f 100644
--- a/content/docs/cookbook/topics/browser-automation.mdx
+++ b/content/docs/cookbook/topics/browser-automation.mdx
@@ -4,8 +4,12 @@ description: "Drive a cloud browser with familiar automation libraries: Playwrig
---
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/content/docs/cookbook/topics/browser-use.mdx b/content/docs/cookbook/topics/browser-use.mdx
index dc4e642b..e45ad6d6 100644
--- a/content/docs/cookbook/topics/browser-use.mdx
+++ b/content/docs/cookbook/topics/browser-use.mdx
@@ -4,7 +4,7 @@ description: Agent recipes built on the browser-use framework.
---
-
-
-
+
+
+
diff --git a/content/docs/cookbook/topics/captchas.mdx b/content/docs/cookbook/topics/captchas.mdx
index 18c39b7e..3b9300ba 100644
--- a/content/docs/cookbook/topics/captchas.mdx
+++ b/content/docs/cookbook/topics/captchas.mdx
@@ -4,6 +4,6 @@ description: "Recipes that handle CAPTCHA challenges using Steel's CAPTCHA API."
---
-
-
+
+
diff --git a/content/docs/cookbook/topics/computer-use.mdx b/content/docs/cookbook/topics/computer-use.mdx
index 1cf8b57b..b5fdefac 100644
--- a/content/docs/cookbook/topics/computer-use.mdx
+++ b/content/docs/cookbook/topics/computer-use.mdx
@@ -4,8 +4,8 @@ description: Model-native browser control where the LLM sees the screen and emit
---
-
-
-
-
+
+
+
+
diff --git a/content/docs/cookbook/topics/convex.mdx b/content/docs/cookbook/topics/convex.mdx
index 0c63c87e..b342d2b8 100644
--- a/content/docs/cookbook/topics/convex.mdx
+++ b/content/docs/cookbook/topics/convex.mdx
@@ -4,6 +4,6 @@ description: Recipes that run Steel from a Convex backend.
---
-
-
+
+
diff --git a/content/docs/cookbook/topics/mcp.mdx b/content/docs/cookbook/topics/mcp.mdx
new file mode 100644
index 00000000..66ed0f4f
--- /dev/null
+++ b/content/docs/cookbook/topics/mcp.mdx
@@ -0,0 +1,8 @@
+---
+title: MCP
+description: 1 recipe tagged MCP.
+---
+
+
+
+
diff --git a/content/docs/cookbook/topics/meta.json b/content/docs/cookbook/topics/meta.json
index 53e7dcc5..83e0bbb7 100644
--- a/content/docs/cookbook/topics/meta.json
+++ b/content/docs/cookbook/topics/meta.json
@@ -8,6 +8,7 @@
"captchas",
"computer-use",
"convex",
+ "mcp",
"mobile",
"nextjs",
"playwright",
diff --git a/content/docs/cookbook/topics/mobile.mdx b/content/docs/cookbook/topics/mobile.mdx
index d417ce90..2927fe64 100644
--- a/content/docs/cookbook/topics/mobile.mdx
+++ b/content/docs/cookbook/topics/mobile.mdx
@@ -4,5 +4,5 @@ description: "Recipes targeting Steel's mobile browser environment."
---
-
+
diff --git a/content/docs/cookbook/topics/nextjs.mdx b/content/docs/cookbook/topics/nextjs.mdx
index 3f1b0ebc..1bce8ae0 100644
--- a/content/docs/cookbook/topics/nextjs.mdx
+++ b/content/docs/cookbook/topics/nextjs.mdx
@@ -4,5 +4,5 @@ description: Recipes that integrate Steel into a Next.js application.
---
-
+
diff --git a/content/docs/cookbook/topics/playwright.mdx b/content/docs/cookbook/topics/playwright.mdx
index d0251029..2d12b3fb 100644
--- a/content/docs/cookbook/topics/playwright.mdx
+++ b/content/docs/cookbook/topics/playwright.mdx
@@ -4,8 +4,8 @@ description: Recipes that use Playwright to drive a Steel session, either as the
---
-
-
-
-
+
+
+
+
diff --git a/content/docs/cookbook/topics/search.mdx b/content/docs/cookbook/topics/search.mdx
index fe2bea0d..7fe97de4 100644
--- a/content/docs/cookbook/topics/search.mdx
+++ b/content/docs/cookbook/topics/search.mdx
@@ -4,5 +4,5 @@ description: Recipes that pair a search API with a Steel browser to keep the age
---
-
+
diff --git a/content/docs/cookbook/topics/steel-apis.mdx b/content/docs/cookbook/topics/steel-apis.mdx
index 17c0c503..a3e737c3 100644
--- a/content/docs/cookbook/topics/steel-apis.mdx
+++ b/content/docs/cookbook/topics/steel-apis.mdx
@@ -4,10 +4,11 @@ description: "Recipes for Steel's first-party APIs: credentials, auth contexts,
---
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/content/docs/cookbook/topics/subagents.mdx b/content/docs/cookbook/topics/subagents.mdx
index a80806ff..7b73ffd2 100644
--- a/content/docs/cookbook/topics/subagents.mdx
+++ b/content/docs/cookbook/topics/subagents.mdx
@@ -4,5 +4,5 @@ description: Multi-agent recipes where a lead orchestrator dispatches parallel s
---
-
+
diff --git a/content/docs/cookbook/topics/typed-output.mdx b/content/docs/cookbook/topics/typed-output.mdx
index bf9172ca..822e5777 100644
--- a/content/docs/cookbook/topics/typed-output.mdx
+++ b/content/docs/cookbook/topics/typed-output.mdx
@@ -4,10 +4,11 @@ description: Agents that return structured, schema-validated results instead of
---
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/content/docs/cookbook/vercel-ai-sdk-nextjs.mdx b/content/docs/cookbook/vercel-ai-sdk-nextjs.mdx
index 2a9391e3..016f2ae5 100644
--- a/content/docs/cookbook/vercel-ai-sdk-nextjs.mdx
+++ b/content/docs/cookbook/vercel-ai-sdk-nextjs.mdx
@@ -3,9 +3,9 @@ title: Stream a browser agent into a Next.js chat app
description: A Next.js App Router chat app where a Vercel AI SDK agent drives a Steel cloud browser with embedded Live View.
---
-
+
-
+
@@ -90,7 +90,7 @@ The `/api/chat` route already declares `maxDuration = 120` and `runtime = "nodej
## Related recipes
-
-
-
+
+
+
diff --git a/content/docs/cookbook/vercel-ai-sdk.mdx b/content/docs/cookbook/vercel-ai-sdk.mdx
index 8a77e98d..9fc2e702 100644
--- a/content/docs/cookbook/vercel-ai-sdk.mdx
+++ b/content/docs/cookbook/vercel-ai-sdk.mdx
@@ -3,9 +3,9 @@ title: Build a typed browser agent with the Vercel AI SDK
description: Use Steel with the Vercel AI SDK v6 ToolLoopAgent for typed, tool-using browser agents.
---
-
+
-
+
@@ -83,7 +83,7 @@ A full run takes ~20 seconds and costs a few cents of Steel session time plus a
## Related recipes
-
-
-
+
+
+
diff --git a/content/docs/cookbook/you-com-search.mdx b/content/docs/cookbook/you-com-search.mdx
index 28e64476..2b3189a9 100644
--- a/content/docs/cookbook/you-com-search.mdx
+++ b/content/docs/cookbook/you-com-search.mdx
@@ -3,9 +3,9 @@ title: Combine You.com search with Steel browser actions
description: Pair the You.com Search and Contents APIs with a Steel cloud browser in a search-then-act LangChain agent that prefers the cheap path and only opens a session when interaction is required.
---
-
+
-
+
@@ -133,7 +133,7 @@ If the agent answers from search and contents alone, the `open_session`, `naviga
## Related recipes
-
-
-
+
+
+
diff --git a/cookbook.lock.json b/cookbook.lock.json
index e0a65a9d..053a30b4 100644
--- a/cookbook.lock.json
+++ b/cookbook.lock.json
@@ -1,5 +1,5 @@
{
"repo": "steel-dev/steel-cookbook",
"ref": "main",
- "sha": "92f29742253e2b6c6801d109e18232768e5291a0"
+ "sha": "e08de0575649d82b60ddfad343541f336589d919"
}
diff --git a/lib/remark-format-code.ts b/lib/remark-format-code.ts
index 976e7ef3..106f3ada 100644
--- a/lib/remark-format-code.ts
+++ b/lib/remark-format-code.ts
@@ -52,6 +52,9 @@ function shouldFormatLanguage(lang: string): boolean {
'python',
'py',
'json',
+ 'go',
+ 'rust',
+ 'rs',
// Add more languages as needed
];
diff --git a/next.config.mjs b/next.config.mjs
index 5ad3ab90..ca0eee76 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -39,6 +39,16 @@ const config = {
destination: "https://pypi.org/project/steel-sdk/",
permanent: true,
},
+ {
+ source: "/steel-go-sdk",
+ destination: "https://pkg.go.dev/github.com/steel-dev/steel-go",
+ permanent: true,
+ },
+ {
+ source: "/steel-rust-sdk",
+ destination: "https://crates.io/crates/steel-rs",
+ permanent: true,
+ },
{
source: "/api-reference",
destination: "https://steel.apidocumentation.com/api-reference",
diff --git a/niko/faq/faq-preview.png b/niko/faq/faq-preview.png
deleted file mode 100644
index 86137508..00000000
Binary files a/niko/faq/faq-preview.png and /dev/null differ
diff --git a/scripts/sync-cookbook.ts b/scripts/sync-cookbook.ts
index bc9780ea..68e0d7c9 100644
--- a/scripts/sync-cookbook.ts
+++ b/scripts/sync-cookbook.ts
@@ -53,7 +53,11 @@ const TOPIC_DESCRIPTIONS: Record = {
// Display order for language tabs in merged concept pages. Entries not
// listed fall back to the end in insertion order.
-const LANGUAGE_ORDER: string[] = ['TypeScript', 'Python', 'Next.js'];
+const LANGUAGE_ORDER: string[] = ['TypeScript', 'Python', 'Rust', 'Go'];
+
+// Curated recipes surfaced in a "Featured" row at the top of the cookbook
+// home page, shown in this order. Slugs must match concept slugs.
+const FEATURED_SLUGS: string[] = ['scrape', 'playwright', 'vercel-ai-sdk', 'claude-agent-sdk'];
interface CookbookLock {
repo: string; // "owner/name" on GitHub
@@ -227,6 +231,20 @@ function conceptTopics(concept: Concept): string[] {
return ordered;
}
+// Distinct languages a concept ships in, in display order (TypeScript,
+// Python, Go, Rust, ...). Drives the language badges on recipe cards.
+function conceptLanguages(concept: Concept): string[] {
+ const seen = new Set();
+ const ordered: string[] = [];
+ for (const entry of concept.entries) {
+ if (entry.language && !seen.has(entry.language)) {
+ seen.add(entry.language);
+ ordered.push(entry.language);
+ }
+ }
+ return ordered;
+}
+
// Earliest first-commit date among a concept's variants. Shown on cards
// and used to sort every recipe grid (home, topic pages, related)
// "newest published first".
@@ -344,9 +362,11 @@ function frontmatter(fields: Record): string {
function renderRecipeCard(concept: Concept): string {
const topics = conceptTopics(concept);
const topicsLiteral = `[${topics.map((t) => `'${t.replace(/'/g, "\\'")}'`).join(', ')}]`;
+ const languages = conceptLanguages(concept);
+ const languagesLiteral = `[${languages.map((l) => `'${l.replace(/'/g, "\\'")}'`).join(', ')}]`;
const date = conceptCreatedDate(concept);
const dateAttr = date ? ` date="${date}"` : '';
- return ``;
+ return ``;
}
function renderRecipeGrid(concepts: Concept[]): string {
@@ -503,15 +523,19 @@ async function emitHome(concepts: Concept[]): Promise {
title: c.title,
description: c.description,
topics: conceptTopics(c),
+ languages: conceptLanguages(c),
date: conceptCreatedDate(c),
}));
const recipesLiteral = JSON.stringify(recipeData, null, 2);
+ const bySlug = new Map(recipeData.map((r) => [r.slug, r]));
+ const featuredData = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter((r) => r !== undefined);
+ const featuredLiteral = JSON.stringify(featuredData, null, 2);
const fm = frontmatter({
title: 'Cookbook',
sidebarTitle: 'Home',
description: 'Runnable recipes for using Steel with your favorite libraries and frameworks.',
});
- const body = ``;
+ const body = ``;
await fs.writeFile(path.join(OUTPUT_DIR, 'index.mdx'), `${fm}\n\n${body}\n`);
}