|
|
|
@@ -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).
|