Next.js Screenshot API: Capture Pages from API Routes and Server Actions
Next.js dominates modern web development. But if you need to generate screenshots, PDFs, or dynamic OG images from your app, you hit a wall:
- Puppeteer — doesn't work on Vercel Edge Runtime
- Sharp — only handles static images, not rendering HTML
- html2canvas — client-side only, can't render from the server
- wkhtmltopdf — requires system binary, not serverless-friendly
You need a solution that works in your API routes and Server Actions, on any Node.js runtime — including serverless and Edge.
That's an API.
The Next.js Problem: Server-Side Screenshots
Next.js is great for building full-stack apps. But screenshot generation requires either:
- Heavy dependencies — Puppeteer in your bundle (bloats your function)
- Platform limitations — solutions that don't work on Edge Runtime
- Architecture mismatches — tools designed for different use cases
Here's what doesn't work in Next.js serverless:
// ❌ This doesn't work on Vercel Edge
import puppeteer from 'puppeteer-core';
export default async function handler(req, res) {
const browser = await puppeteer.launch(); // ❌ Chrome binary not available on Edge
// ...
}
// ❌ This doesn't work on serverless
import { execSync } from 'child_process';
export default async function handler(req, res) {
execSync('wkhtmltopdf input.html output.pdf'); // ❌ Binary not in serverless environment
// ...
}
Here's what does work: a screenshot API.
Next.js API Route Example
Dynamic Screenshots from Your App
// app/api/screenshot/route.js
import { NextResponse } from 'next/server';
const PAGEBOLT_KEY = process.env.PAGEBOLT_API_KEY;
export async function POST(request) {
const { url, width = 1280, height = 720 } = await request.json();
// Call PageBolt from your API route
const response = await fetch('https://api.pagebolt.dev/take_screenshot', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PAGEBOLT_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url, width, height })
});
if (!response.ok) {
return NextResponse.json({ error: 'Screenshot failed' }, { status: 500 });
}
const data = await response.json();
return NextResponse.json({ imageUrl: data.imageUrl });
}
Usage from the client:
// pages or client component
const takeScreenshot = async (url) => {
const response = await fetch('/api/screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const { imageUrl } = await response.json();
return imageUrl;
};
Dynamic OG Image Generation
Generate unique OG images for every page using Server Actions and dynamic routes:
// app/api/og/route.js
import { ImageResponse } from 'next/og';
export const runtime = 'edge'; // Works on Edge Runtime
export async function GET(request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title') || 'My Page';
const author = searchParams.get('author') || 'Author';
// Generate OG image directly in Edge Runtime
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontFamily: 'system-ui'
}}
>
<div style={{ textAlign: 'center' }}>
<h1 style={{ margin: 0, fontSize: 56 }}>{title}</h1>
<p style={{ marginTop: 20, fontSize: 32, opacity: 0.8 }}>By {author}</p>
</div>
</div>
),
{
width: 1200,
height: 630
}
);
}
But what if you need full Chromium rendering? Use PageBolt:
// app/api/og-advanced/route.js
const PAGEBOLT_KEY = process.env.PAGEBOLT_API_KEY;
export async function GET(request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title') || 'My Page';
const author = searchParams.get('author') || 'Author';
const html = `
<html>
<style>
body {
width: 1200px;
height: 630px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-family: system-ui;
margin: 0;
}
h1 { font-size: 56px; margin: 0; }
p { margin-top: 20px; font-size: 32px; opacity: 0.8; }
</style>
<body>
<div>
<h1>${title}</h1>
<p>By ${author}</p>
</div>
</body>
</html>
`;
const response = await fetch('https://api.pagebolt.dev/take_screenshot', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PAGEBOLT_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ html, width: 1200, height: 630 })
});
if (!response.ok) {
return new Response('OG image generation failed', { status: 500 });
}
const data = await response.json();
const imageResponse = await fetch(data.imageUrl);
return new Response(imageResponse.body, {
status: 200,
headers: { 'Content-Type': 'image/png' }
});
}
Server Actions Example
Generate PDFs or screenshots directly from Server Actions:
// app/actions/screenshot.js
'use server';
const PAGEBOLT_KEY = process.env.PAGEBOLT_API_KEY;
export async function generateInvoicePDF(invoiceData) {
// Render invoice HTML (from template or database)
const html = `
<html>
<style>
body { font-family: Arial; padding: 40px; }
.header { font-size: 24px; font-weight: bold; }
.items { margin-top: 30px; }
</style>
<body>
<div class="header">Invoice #${invoiceData.id}</div>
<div class="items">
${invoiceData.items.map(item => `<p>${item.name}: $${item.price}</p>`).join('')}
</div>
</body>
</html>
`;
// Convert to PDF
const response = await fetch('https://api.pagebolt.dev/generate_pdf', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PAGEBOLT_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ html, format: 'A4', margin: '10mm' })
});
if (!response.ok) {
throw new Error('PDF generation failed');
}
const data = await response.json();
return data.pdfUrl;
}
// Use in a Client Component
'use client';
import { generateInvoicePDF } from '@/app/actions/screenshot';
export default function InvoiceButton() {
const handleGeneratePDF = async () => {
const pdfUrl = await generateInvoicePDF({
id: '12345',
items: [
{ name: 'Product A', price: 100 },
{ name: 'Product B', price: 50 }
]
});
// Download or display PDF
window.open(pdfUrl);
};
return <button onClick={handleGeneratePDF}>Download Invoice</button>;
}
Real-World Use Cases
1. Social Preview Images
Generate unique OG images for every blog post, product, or page share:
// app/[slug]/opengraph-image.js (Next.js built-in)
export const runtime = 'edge';
export const alt = 'Post preview';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default function OG({ params }) {
// For simple designs, use Next.js ImageResponse
// For complex designs, call PageBolt from here or from an API route
}
2. Report PDFs
Generate reports on-demand from user data:
// app/api/reports/generate/route.js
export async function POST(request) {
const { userId, dateRange } = await request.json();
// Fetch user data
const userData = await db.users.findById(userId);
// Render report HTML
const html = renderReportTemplate(userData, dateRange);
// Convert to PDF
const pdfUrl = await generatePDF(html);
// Store reference in database
await db.reports.create({ userId, pdfUrl });
return NextResponse.json({ pdfUrl });
}
3. Dynamic Certificates
Generate achievement certificates with dynamic data:
// app/api/certificates/generate/route.js
export async function POST(request) {
const { userName, achievement, date } = await request.json();
const html = `
<html>
<style>
body {
width: 1000px;
height: 700px;
background: linear-gradient(135deg, #fff 0%, #f9f9f9 100%);
border: 5px solid #gold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-family: Georgia;
}
h1 { font-size: 48px; margin: 20px 0; }
.achievement { font-size: 32px; margin: 30px 0; }
.date { font-size: 18px; margin-top: 40px; color: #666; }
</style>
<body>
<h1>Certificate of Achievement</h1>
<p style="font-size: 24px;">This is to certify that</p>
<h2 style="font-size: 36px;">${userName}</h2>
<p style="font-size: 24px;">has successfully completed</p>
<div class="achievement">${achievement}</div>
<div class="date">${date}</div>
</body>
</html>
`;
const certificateUrl = await generateScreenshot(html);
return NextResponse.json({ certificateUrl });
}
Configuration
In your .env.local:
PAGEBOLT_API_KEY=your_api_key_here
Why PageBolt for Next.js
| Aspect | Puppeteer | Sharp | next/og | PageBolt API |
|---|---|---|---|---|
| Works on Edge Runtime | ❌ No | ✅ Yes (images only) | ✅ Yes | ✅ Yes |
| Full HTML rendering | ✅ Yes | ❌ No | ⚠️ Limited | ✅ Yes |
| Serverless-friendly | ❌ Heavy | ✅ Light | ✅ Light | ✅ Light |
| Dynamic OG images | ⚠️ Possible | ❌ No | ✅ Yes | ✅ Yes |
| PDF generation | ✅ Yes | ❌ No | ❌ No | ✅ Yes |
| Setup complexity | High | Low | Low | Minimal |
| Dependencies | Heavy | None | None | None (HTTP) |
Getting Started
1. Get API key (free tier: 100 requests/month)
# Visit pagebolt.dev, create account, copy key
2. Set environment variable
# .env.local
PAGEBOLT_API_KEY=your_key_here
3. Create an API route
// app/api/screenshot/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const { url } = await request.json();
const response = await fetch('https://api.pagebolt.dev/take_screenshot', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
const data = await response.json();
return NextResponse.json({ imageUrl: data.imageUrl });
}
4. Use from your app
const imageUrl = await fetch('/api/screenshot', {
method: 'POST',
body: JSON.stringify({ url: 'https://example.com' })
}).then(r => r.json());
Next Steps
- Try PageBolt free — 100 requests/month, no credit card.
- Start with OG images — most common use case, easiest to implement.
- Add PDF generation — invoices, reports, certificates.
- Scale instantly — API handles all infrastructure.
Stop wrestling with Puppeteer and binaries. Start generating screenshots and PDFs in Next.js.
PageBolt: Screenshots and PDFs for Next.js, serverless-ready. Get started free →
Top comments (0)