Loading...
Loading...
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.
npx skill4agent add vercel-labs/agent-browser next@sparticuz/chromiumpuppeteer-corepnpm add @sparticuz/chromium puppeteer-coreimport 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 },
});
}@sparticuz/chromiumCHROMIUM_PATH"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();
}
}"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();
}
}// 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();
}
}| Variable | Required | Description |
|---|---|---|
| Local dev only | Path to Chrome/Chromium binary. Not needed on Vercel. |
@sparticuz/chromiumCHROMIUM_PATH@sparticuz/chromium// next.config.ts
const nextConfig = {
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;outputFileTracingRootimport path from "node:path";
const nextConfig = {
outputFileTracingRoot: path.join(import.meta.dirname, "../../"),
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;@sparticuz/chromiumvercel-sandboxexamples/demo/