next.js example (#694)

* next.js guide

* better

* shadcn

* fixes

* fix: correct screenshot test assertion to check path instead of base64

The daemon returns { path: savePath } for screenshot commands, not base64.

* fix: cross-platform Chrome detection and gitignore hardening

- Replace hardcoded macOS Chrome path with findLocalChrome() that
  searches common paths on macOS, Linux, and WSL, with a clear error
  message when no Chrome is found.
- Add .env and .env*.local to .gitignore to prevent accidental
  secret commits.

* fix: correct Vercel deploy button repo URL to vercel-labs/agent-browser

* clean up

* demo

* next page

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-03-09 15:24:44 -05:00
committed by GitHub
co-authored by ctate
parent 3649787268
commit cc3c70dc86
35 changed files with 6764 additions and 2 deletions
+207
View File
@@ -0,0 +1,207 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("next")
# Next.js + Vercel
Two patterns for running agent-browser from Next.js on Vercel.
## Pattern 1: Serverless Function
Run `@sparticuz/chromium` + `puppeteer-core` directly inside a Vercel
serverless function. No external server needed.
### Setup
```bash
pnpm add @sparticuz/chromium puppeteer-core
```
### Browser launcher
```ts
import puppeteer from "puppeteer-core";
import chromium from "@sparticuz/chromium";
import fs from "node:fs";
const CHROME_PATHS = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/usr/bin/google-chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
];
function findLocalChrome(): string {
for (const p of CHROME_PATHS) {
if (fs.existsSync(p)) return p;
}
throw new Error("Chrome not found. Set CHROMIUM_PATH.");
}
async function launchBrowser() {
const isLambda =
!!process.env.VERCEL || !!process.env.AWS_LAMBDA_FUNCTION_NAME;
return puppeteer.launch({
args: isLambda ? chromium.args : ["--no-sandbox"],
executablePath: isLambda
? await chromium.executablePath()
: process.env.CHROMIUM_PATH || findLocalChrome(),
headless: true,
defaultViewport: { width: 1280, height: 720 },
});
}
```
On Vercel, `@sparticuz/chromium` provides the binary automatically.
Locally, the launcher finds your system Chrome or uses `CHROMIUM_PATH`.
### Server action
```ts
"use server";
export async function screenshotUrl(url: string) {
const browser = await launchBrowser();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 });
const title = await page.title();
const screenshot = await page.screenshot({ encoding: "base64" });
return { ok: true, title, screenshot };
} finally {
await browser.close();
}
}
```
### Next.js config
```ts
// next.config.ts
const nextConfig = {
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;
```
## Pattern 2: Vercel Sandbox
A Linux microVM spins up on demand, runs agent-browser + Chrome, and
shuts down. No binary size limits. Best for multi-step workflows or
when you need full Chrome capabilities.
### Server action
```ts
"use server";
import { Sandbox } from "@vercel/sandbox";
export async function screenshotUrl(url: string) {
const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;
const sandbox = snapshotId
? await Sandbox.create({
source: { type: "snapshot", snapshotId },
timeout: 120_000,
})
: await Sandbox.create({ runtime: "node24", timeout: 120_000 });
try {
if (!snapshotId) {
await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]);
await sandbox.runCommand("npx", ["agent-browser", "install"]);
}
await sandbox.runCommand("agent-browser", ["open", url]);
const result = await sandbox.runCommand("agent-browser", [
"screenshot", "--json",
]);
const stdout = await result.stdout();
const data = JSON.parse(stdout);
await sandbox.runCommand("agent-browser", ["close"]);
return { ok: true, screenshot: data.data.base64 };
} finally {
await sandbox.stop();
}
}
```
### Optimize with snapshots
Installing agent-browser + Chromium takes ~30 seconds. Create a
snapshot to make sandbox creation sub-second:
```bash
npx tsx scripts/create-snapshot.ts
# Output: AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx
```
Add the snapshot ID to your Vercel environment variables or `.env.local`.
## Scheduled workflows (cron)
For recurring tasks like daily monitoring, use Vercel Cron Jobs with
either pattern:
```ts
// app/api/cron/monitor/route.ts
export async function GET() {
const browser = await launchBrowser();
try {
const page = await browser.newPage();
await page.goto("https://example.com/pricing", {
waitUntil: "networkidle2",
});
const snapshot = await page.accessibility.snapshot();
// Process results, send alerts, store in database...
return Response.json({ ok: true, snapshot });
} finally {
await browser.close();
}
}
```
```json
// vercel.json
{
"crons": [
{ "path": "/api/cron/monitor", "schedule": "0 9 * * *" }
]
}
```
## When to use which pattern
<table>
<thead>
<tr><th></th><th>Serverless Function</th><th>Vercel Sandbox</th></tr>
</thead>
<tbody>
<tr><td>Infrastructure</td><td>None (runs in-function)</td><td>None (Vercel manages VMs)</td></tr>
<tr><td>Startup time</td><td>~2-3 seconds</td><td>Sub-second with snapshot</td></tr>
<tr><td>Binary size limit</td><td>50MB compressed</td><td>None</td></tr>
<tr><td>Multi-step workflows</td><td>Single request only</td><td>Yes (persistent session)</td></tr>
<tr><td>Task duration</td><td>Up to 300s (Pro)</td><td>Up to ~2 minutes</td></tr>
<tr><td>Best for</td><td>Fast single-request screenshots</td><td>Complex workflows, full Chrome</td></tr>
</tbody>
</table>
## Environment variables
<table>
<thead>
<tr><th>Variable</th><th>Pattern</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>CHROMIUM_PATH</code></td><td>Serverless</td><td>Path to local Chrome/Chromium (auto-detected on Vercel)</td></tr>
<tr><td><code>AGENT_BROWSER_SNAPSHOT_ID</code></td><td>Sandbox</td><td>Pre-built snapshot for fast startup</td></tr>
</tbody>
</table>
## Demo app
A working demo with both patterns, a UI, and a deploy-to-Vercel button is at
[`examples/demo/`](https://github.com/agent-browser/agent-browser/tree/main/examples/demo).
+1 -1
View File
@@ -66,7 +66,7 @@ export function Header() {
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span>16k</span>
<span>20k</span>
</a>
<a
href="https://www.npmjs.com/package/agent-browser"
+1
View File
@@ -37,6 +37,7 @@ export const navigation: NavSection[] = [
{ name: "Profiler", href: "/profiler" },
{ name: "iOS Simulator", href: "/ios" },
{ name: "Security", href: "/security" },
{ name: "Next.js + Vercel", href: "/next" },
{ name: "Native Mode (Experimental)", href: "/native-mode" },
],
},
+1
View File
@@ -16,6 +16,7 @@ export const PAGE_TITLES: Record<string, string> = {
security: "Security",
"engines/chrome": "Chrome",
"engines/lightpanda": "Lightpanda",
next: "Next.js + Vercel",
"native-mode": "Native Mode (Experimental)",
changelog: "Changelog",
};