DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Next.js Screenshot API: Capture Pages from API Routes and Server Actions

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:

  1. Heavy dependencies — Puppeteer in your bundle (bloats your function)
  2. Platform limitations — solutions that don't work on Edge Runtime
  3. 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
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

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' }
  });
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

Configuration

In your .env.local:

PAGEBOLT_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. Set environment variable

# .env.local
PAGEBOLT_API_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

4. Use from your app

const imageUrl = await fetch('/api/screenshot', {
  method: 'POST',
  body: JSON.stringify({ url: 'https://example.com' })
}).then(r => r.json());
Enter fullscreen mode Exit fullscreen mode

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)