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:
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: next
|
||||
description: Run headless Chrome in Next.js serverless functions using @sparticuz/chromium + puppeteer-core. Use when the user needs browser automation from a Next.js app, wants to take screenshots or snapshots from server actions or API routes, or is building a Next.js app that needs headless Chrome. Triggers include "screenshot from Next.js", "headless Chrome in serverless", "browser automation in Next.js", "puppeteer on Vercel", or any task requiring Chrome in a Next.js server context.
|
||||
---
|
||||
|
||||
# Browser Automation in Next.js Serverless Functions
|
||||
|
||||
Run headless Chrome directly inside Next.js server actions and API routes using `@sparticuz/chromium` + `puppeteer-core`. No external server needed -- Chrome runs in the same serverless function.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```bash
|
||||
pnpm add @sparticuz/chromium puppeteer-core
|
||||
```
|
||||
|
||||
## Core Pattern
|
||||
|
||||
```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/google-chrome-stable",
|
||||
"/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 to your Chrome/Chromium binary.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function launchBrowser() {
|
||||
const isLambda =
|
||||
!!process.env.VERCEL || !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
||||
|
||||
const executablePath = isLambda
|
||||
? await chromium.executablePath()
|
||||
: process.env.CHROMIUM_PATH || findLocalChrome();
|
||||
|
||||
const args = isLambda
|
||||
? chromium.args
|
||||
: ["--no-sandbox", "--disable-setuid-sandbox"];
|
||||
|
||||
return puppeteer.launch({
|
||||
args,
|
||||
executablePath,
|
||||
headless: true,
|
||||
defaultViewport: { width: 1280, height: 720 },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
On Vercel, `@sparticuz/chromium` bundles a compatible Chromium binary automatically. Locally, the launcher falls back to the system Chrome installation or `CHROMIUM_PATH`.
|
||||
|
||||
## Server Actions
|
||||
|
||||
### Screenshot
|
||||
|
||||
```ts
|
||||
"use server";
|
||||
|
||||
export async function takeScreenshot(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({
|
||||
fullPage: true,
|
||||
encoding: "base64",
|
||||
});
|
||||
return { ok: true, title, screenshot: screenshot as string };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accessibility Snapshot
|
||||
|
||||
```ts
|
||||
"use server";
|
||||
|
||||
export async function takeSnapshot(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 snapshot = await page.accessibility.snapshot();
|
||||
return { ok: true, title, snapshot: JSON.stringify(snapshot, null, 2) };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
```ts
|
||||
// app/api/browse/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { url, action } = await req.json();
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "Provide a 'url'" }, { status: 400 });
|
||||
}
|
||||
|
||||
const browser = await launchBrowser();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 });
|
||||
|
||||
if (action === "screenshot") {
|
||||
const screenshot = await page.screenshot({ encoding: "base64" });
|
||||
return NextResponse.json({ screenshot });
|
||||
}
|
||||
|
||||
if (action === "snapshot") {
|
||||
const snapshot = await page.accessibility.snapshot();
|
||||
return NextResponse.json({ snapshot });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "action must be 'screenshot' or 'snapshot'" },
|
||||
{ status: 400 },
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `CHROMIUM_PATH` | Local dev only | Path to Chrome/Chromium binary. Not needed on Vercel. |
|
||||
|
||||
On Vercel, `@sparticuz/chromium` auto-detects the bundled binary. Locally, if Chrome is not in a standard location, set `CHROMIUM_PATH`.
|
||||
|
||||
## Vercel Configuration
|
||||
|
||||
The `@sparticuz/chromium` binary is large (~50MB). Increase the serverless function's memory and timeout if needed:
|
||||
|
||||
```ts
|
||||
// next.config.ts
|
||||
const nextConfig = {
|
||||
serverExternalPackages: ["@sparticuz/chromium"],
|
||||
};
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
If the project lives in a monorepo subdirectory, set `outputFileTracingRoot` so the Chromium binary is included in the deployment:
|
||||
|
||||
```ts
|
||||
import path from "node:path";
|
||||
|
||||
const nextConfig = {
|
||||
outputFileTracingRoot: path.join(import.meta.dirname, "../../"),
|
||||
serverExternalPackages: ["@sparticuz/chromium"],
|
||||
};
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Vercel serverless functions have a 50MB compressed size limit. `@sparticuz/chromium` fits within this but leaves limited room for other large dependencies.
|
||||
- Function execution timeout is 10s on Hobby, 300s on Pro. Complex page loads may need the Pro plan.
|
||||
- Each invocation launches a fresh browser. There is no session persistence between requests.
|
||||
- For workflows that need persistent sessions, longer timeouts, or full Chrome (no size limits), use the Vercel Sandbox pattern instead (see the `vercel-sandbox` skill).
|
||||
|
||||
## Example
|
||||
|
||||
See `examples/demo/` in the agent-browser repo for a working app with both serverless and sandbox patterns, and a deploy-to-Vercel button.
|
||||
Reference in New Issue
Block a user