Move JS metadata exports out of page.mdx files into per-directory layout.tsx files. MDX files now contain pure markdown content, making them render cleanly on GitHub without visible JS import/export lines. No content changes — only the metadata mechanism is relocated. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
6.2 KiB
Plaintext
208 lines
6.2 KiB
Plaintext
# Next.js + Vercel
|
|
|
|
Run agent-browser from a Next.js app on Vercel using Vercel Sandbox.
|
|
A Linux microVM spins up on demand, runs agent-browser + Chrome, and
|
|
shuts down. No binary size limits, no Chromium bundling complexity.
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
pnpm add @vercel/sandbox
|
|
```
|
|
|
|
## Server action
|
|
|
|
The Vercel Sandbox runs Amazon Linux. Chromium requires system libraries
|
|
that are not installed by default, so fresh sandboxes need a `dnf install`
|
|
step before agent-browser can launch Chrome. Use a sandbox snapshot
|
|
(below) to skip this entirely in production.
|
|
|
|
```ts
|
|
"use server";
|
|
import { Sandbox } from "@vercel/sandbox";
|
|
|
|
const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;
|
|
|
|
const CHROMIUM_SYSTEM_DEPS = [
|
|
"nss", "nspr", "libxkbcommon", "atk", "at-spi2-atk", "at-spi2-core",
|
|
"libXcomposite", "libXdamage", "libXrandr", "libXfixes", "libXcursor",
|
|
"libXi", "libXtst", "libXScrnSaver", "libXext", "mesa-libgbm", "libdrm",
|
|
"mesa-libGL", "mesa-libEGL", "cups-libs", "alsa-lib", "pango", "cairo",
|
|
"gtk3", "dbus-libs",
|
|
];
|
|
|
|
function getSandboxCredentials() {
|
|
if (
|
|
process.env.VERCEL_TOKEN &&
|
|
process.env.VERCEL_TEAM_ID &&
|
|
process.env.VERCEL_PROJECT_ID
|
|
) {
|
|
return {
|
|
token: process.env.VERCEL_TOKEN,
|
|
teamId: process.env.VERCEL_TEAM_ID,
|
|
projectId: process.env.VERCEL_PROJECT_ID,
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function withBrowser<T>(
|
|
fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,
|
|
): Promise<T> {
|
|
const credentials = getSandboxCredentials();
|
|
|
|
const sandbox = snapshotId
|
|
? await Sandbox.create({
|
|
...credentials,
|
|
source: { type: "snapshot", snapshotId },
|
|
timeout: 120_000,
|
|
})
|
|
: await Sandbox.create({ ...credentials, runtime: "node24", timeout: 120_000 });
|
|
|
|
if (!snapshotId) {
|
|
await sandbox.runCommand("sh", [
|
|
"-c",
|
|
`sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`,
|
|
]);
|
|
await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]);
|
|
await sandbox.runCommand("npx", ["agent-browser", "install"]);
|
|
}
|
|
|
|
try {
|
|
return await fn(sandbox);
|
|
} finally {
|
|
await sandbox.stop();
|
|
}
|
|
}
|
|
|
|
export async function screenshotUrl(url: string) {
|
|
return withBrowser(async (sandbox) => {
|
|
await sandbox.runCommand("agent-browser", ["open", url]);
|
|
|
|
const ssResult = await sandbox.runCommand("agent-browser", [
|
|
"screenshot", "--json",
|
|
]);
|
|
const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;
|
|
const b64Result = await sandbox.runCommand("base64", ["-w", "0", ssPath]);
|
|
const screenshot = (await b64Result.stdout()).trim();
|
|
|
|
await sandbox.runCommand("agent-browser", ["close"]);
|
|
return { ok: true, screenshot };
|
|
});
|
|
}
|
|
|
|
export async function snapshotUrl(url: string) {
|
|
return withBrowser(async (sandbox) => {
|
|
await sandbox.runCommand("agent-browser", ["open", url]);
|
|
|
|
const result = await sandbox.runCommand("agent-browser", [
|
|
"snapshot", "-i", "-c",
|
|
]);
|
|
const snapshot = await result.stdout();
|
|
|
|
await sandbox.runCommand("agent-browser", ["close"]);
|
|
return { ok: true, snapshot };
|
|
});
|
|
}
|
|
```
|
|
|
|
## Sandbox snapshots
|
|
|
|
Without optimization, each Sandbox run installs system dependencies +
|
|
agent-browser + Chromium from scratch (~30 seconds). A **sandbox snapshot**
|
|
is a saved VM image with everything pre-installed -- like a Docker image
|
|
for Vercel Sandbox. When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox
|
|
boots from that image instead of installing, bringing startup down to
|
|
sub-second.
|
|
|
|
This is different from an agent-browser *accessibility snapshot* (which
|
|
dumps a page's accessibility tree). A sandbox snapshot is a Vercel
|
|
infrastructure concept.
|
|
|
|
Create a sandbox snapshot by running the helper script once:
|
|
|
|
```bash
|
|
npx tsx scripts/create-snapshot.ts
|
|
```
|
|
|
|
The script spins up a fresh sandbox, installs system dependencies +
|
|
agent-browser + Chromium, saves the VM state, and prints the snapshot ID:
|
|
|
|
```
|
|
AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx
|
|
```
|
|
|
|
Add this to your Vercel project environment variables (or `.env.local`
|
|
for local development). Recommended for any production deployment.
|
|
|
|
## Authentication
|
|
|
|
On Vercel deployments, the Sandbox SDK authenticates automatically via
|
|
OIDC. For local development, provide explicit credentials:
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Variable</th><th>Description</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token</td></tr>
|
|
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID</td></tr>
|
|
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
When all three are set, they are passed to `Sandbox.create()`. When
|
|
absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).
|
|
|
|
## Scheduled workflows (cron)
|
|
|
|
For recurring tasks like daily monitoring, use Vercel Cron Jobs:
|
|
|
|
```ts
|
|
// app/api/cron/monitor/route.ts
|
|
export async function GET() {
|
|
const result = await withBrowser(async (sandbox) => {
|
|
await sandbox.runCommand("agent-browser", [
|
|
"open", "https://example.com/pricing",
|
|
]);
|
|
const snap = await sandbox.runCommand("agent-browser", [
|
|
"snapshot", "-i", "-c",
|
|
]);
|
|
await sandbox.runCommand("agent-browser", ["close"]);
|
|
return await snap.stdout();
|
|
});
|
|
|
|
// Process results, send alerts, store data...
|
|
return Response.json({ ok: true, snapshot: result });
|
|
}
|
|
```
|
|
|
|
```json
|
|
// vercel.json
|
|
{
|
|
"crons": [
|
|
{ "path": "/api/cron/monitor", "schedule": "0 9 * * *" }
|
|
]
|
|
}
|
|
```
|
|
|
|
## Environment variables
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Variable</th><th>Description</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>AGENT_BROWSER_SNAPSHOT_ID</code></td><td>Sandbox snapshot ID for sub-second startup (see above)</td></tr>
|
|
<tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token (for local dev; OIDC is automatic on Vercel)</td></tr>
|
|
<tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID (for local dev)</td></tr>
|
|
<tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID (for local dev)</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
## Demo app
|
|
|
|
A working demo with streaming progress UI, rate limiting, and a
|
|
deploy-to-Vercel button is at
|
|
[`examples/environments/`](https://github.com/vercel-labs/agent-browser/tree/main/examples/environments).
|