Vercel / Next.js Integration
Generate screenshots in your Next.js app deployed on Vercel using API routes and server components.
Overview
This guide shows how to integrate ScreenshotAPI into a Next.js application deployed on Vercel. You'll create an API route that proxies screenshot requests, keeping your API key secure on the server.
Setup
Add your API key to environment variables
In your Vercel project settings or .env.local:
SCREENSHOTAPI_KEY=sk_live_your_key_hereCreate the API route
Create app/api/screenshot/route.ts:
import { NextResponse, type NextRequest } from 'next/server'
const SCREENSHOTAPI_KEY = process.env.SCREENSHOTAPI_KEY!
const BASE_URL = 'https://screenshotapi.to'
export async function GET(request: NextRequest) {
const url = request.nextUrl.searchParams.get('url')
if (!url) {
return NextResponse.json({ error: 'url is required' }, { status: 400 })
}
const params = new URLSearchParams({ url })
// Forward optional parameters
const optionalParams = [
'width', 'height', 'fullPage', 'type', 'quality',
'colorScheme', 'waitUntil', 'waitForSelector', 'delay'
]
for (const param of optionalParams) {
const value = request.nextUrl.searchParams.get(param)
if (value) params.set(param, value)
}
try {
const response = await fetch(
`${BASE_URL}/api/v1/screenshot?${params}`,
{ headers: { 'x-api-key': SCREENSHOTAPI_KEY } }
)
if (!response.ok) {
const error = await response.json()
return NextResponse.json(error, { status: response.status })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
'Content-Type': response.headers.get('content-type') ?? 'image/png',
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
'x-credits-remaining': response.headers.get('x-credits-remaining') ?? '',
'x-duration-ms': response.headers.get('x-duration-ms') ?? ''
}
})
} catch {
return NextResponse.json({ error: 'Screenshot failed' }, { status: 500 })
}
}Use from your frontend
function ScreenshotImage({ url }: { url: string }) {
const src = `/api/screenshot?url=${encodeURIComponent(url)}&type=webp&quality=80`
return (
<img
src={src}
alt={`Screenshot of ${url}`}
loading="lazy"
className="rounded-lg border shadow-sm"
/>
)
}Server Component Usage
Fetch screenshots directly in React Server Components:
async function ServerScreenshot({ url }: { url: string }) {
const params = new URLSearchParams({
url,
type: 'webp',
quality: '80'
})
const response = await fetch(
`https://screenshotapi.to/api/v1/screenshot?${params}`,
{
headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! },
next: { revalidate: 3600 }
}
)
if (!response.ok) {
return <div className="text-red-500">Failed to load screenshot</div>
}
const buffer = await response.arrayBuffer()
const base64 = Buffer.from(buffer).toString('base64')
const contentType = response.headers.get('content-type') ?? 'image/webp'
const dataUri = `data:${contentType};base64,${base64}`
return (
<img
src={dataUri}
alt={`Screenshot of ${url}`}
className="rounded-lg border shadow-sm"
/>
)
}Using next: { revalidate: 3600 } caches the screenshot for 1 hour with Next.js ISR, reducing API calls and credit usage.
Caching Strategies
Edge Caching with Vercel
Set appropriate cache headers to leverage Vercel's edge network:
return new NextResponse(buffer, {
headers: {
'Content-Type': contentType,
// Cache for 1 hour, serve stale for 24 hours while revalidating
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
// Vary by query params so different URLs get different cache entries
'Vary': 'Accept'
}
})In-Memory Cache with Map
For high-frequency screenshots of the same URLs:
const cache = new Map<string, { buffer: ArrayBuffer; contentType: string; timestamp: number }>()
const CACHE_TTL = 60 * 60 * 1000 // 1 hour
async function cachedScreenshot(url: string): Promise<{ buffer: ArrayBuffer; contentType: string }> {
const cached = cache.get(url)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return { buffer: cached.buffer, contentType: cached.contentType }
}
const response = await fetch(
`https://screenshotapi.to/api/v1/screenshot?url=${encodeURIComponent(url)}`,
{ headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! } }
)
const buffer = await response.arrayBuffer()
const contentType = response.headers.get('content-type') ?? 'image/png'
cache.set(url, { buffer, contentType, timestamp: Date.now() })
return { buffer, contentType }
}Vercel Function Configuration
For long pages or slow-loading sites, increase the function timeout:
// app/api/screenshot/route.ts
export const maxDuration = 30 // seconds (available on Pro plan)Vercel Hobby plans have a 10-second function timeout. If your screenshots take longer (full-page captures of large sites), consider upgrading to Pro for the 60-second limit.
Full Example: Link Preview Component
A complete link preview component that generates and caches thumbnails:
import { Suspense } from 'react'
function LinkPreviewSkeleton() {
return (
<div className="animate-pulse rounded-lg border bg-muted h-[200px] w-full" />
)
}
async function LinkPreviewImage({ url }: { url: string }) {
const params = new URLSearchParams({
url,
width: '800',
height: '600',
type: 'webp',
quality: '75'
})
const response = await fetch(
`https://screenshotapi.to/api/v1/screenshot?${params}`,
{
headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! },
next: { revalidate: 86400 }
}
)
if (!response.ok) return null
const buffer = Buffer.from(await response.arrayBuffer())
const base64 = buffer.toString('base64')
const src = `data:image/webp;base64,${base64}`
return (
<img src={src} alt="" className="rounded-lg object-cover w-full h-[200px]" />
)
}
export function LinkPreview({ url, title }: { url: string; title: string }) {
return (
<a href={url} className="block group cursor-pointer">
<div className="overflow-hidden rounded-lg border transition-shadow group-hover:shadow-md">
<Suspense fallback={<LinkPreviewSkeleton />}>
<LinkPreviewImage url={url} />
</Suspense>
<div className="p-3">
<p className="font-medium text-sm truncate">{title}</p>
<p className="text-xs text-muted-foreground truncate">{url}</p>
</div>
</div>
</a>
)
}